Passed
Push — master ( be3192...a784d9 )
by Doug
25:13
created

GeographicPoint::madridToED50Polynomial()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 16
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 16
ccs 4
cts 4
cp 1
rs 10
cc 3
nc 1
nop 10
crap 3

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 get_class;
22
use function hypot;
23
use function implode;
24
use function is_nan;
25
use function log;
26
use const M_E;
27
use const M_PI;
28
use function max;
29
use PHPCoord\CoordinateOperation\AutoConversion;
30
use PHPCoord\CoordinateOperation\ComplexNumber;
31
use PHPCoord\CoordinateOperation\ConvertiblePoint;
32
use PHPCoord\CoordinateOperation\GeocentricValue;
33
use PHPCoord\CoordinateOperation\GeographicValue;
34
use PHPCoord\CoordinateOperation\NADCON5Grid;
35
use PHPCoord\CoordinateOperation\NTv2Grid;
36
use PHPCoord\CoordinateOperation\OSTNOSGM15Grid;
37
use PHPCoord\CoordinateReferenceSystem\Compound;
38
use PHPCoord\CoordinateReferenceSystem\Geocentric;
39
use PHPCoord\CoordinateReferenceSystem\Geographic;
40
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
41
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
42
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...
43
use PHPCoord\CoordinateSystem\Axis;
44
use PHPCoord\CoordinateSystem\Cartesian;
45
use PHPCoord\Datum\Datum;
46
use PHPCoord\Datum\Ellipsoid;
47
use PHPCoord\Exception\InvalidCoordinateReferenceSystemException;
48
use PHPCoord\Exception\UnknownAxisException;
49
use PHPCoord\Geometry\BoundingArea;
50
use PHPCoord\UnitOfMeasure\Angle\Angle;
51
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
52
use PHPCoord\UnitOfMeasure\Angle\Degree;
53
use PHPCoord\UnitOfMeasure\Angle\Radian;
54
use PHPCoord\UnitOfMeasure\Length\Length;
55
use PHPCoord\UnitOfMeasure\Length\Metre;
56
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
57
use PHPCoord\UnitOfMeasure\Scale\Scale;
58
use PHPCoord\UnitOfMeasure\Scale\Unity;
59
use function sin;
60
use function sinh;
61
use function sprintf;
62
use function sqrt;
63
use function tan;
64
use TypeError;
65
66
/**
67
 * Coordinate representing a point on an ellipsoid.
68
 */
69
class GeographicPoint extends Point implements ConvertiblePoint
70
{
71
    use AutoConversion;
72
73
    /**
74
     * Latitude.
75
     */
76
    protected Angle $latitude;
77
78
    /**
79
     * Longitude.
80
     */
81
    protected Angle $longitude;
82
83
    /**
84
     * Height above ellipsoid (N.B. *not* height above ground, sea-level or anything else tangible).
85
     */
86
    protected ?Length $height;
87
88
    /**
89
     * Coordinate reference system.
90
     */
91
    protected Geographic $crs;
92
93
    /**
94
     * Coordinate epoch (date for which the specified coordinates represented this point).
95
     */
96
    protected ?DateTimeImmutable $epoch;
97
98 1476
    protected function __construct(Angle $latitude, Angle $longitude, ?Length $height, Geographic $crs, ?DateTimeInterface $epoch = null)
99
    {
100 1476
        if (!$crs instanceof Geographic2D && !$crs instanceof Geographic3D) {
101
            throw new TypeError(sprintf("A geographic point must be associated with a geographic CRS, but a '%s' CRS was given", get_class($crs)));
102
        }
103
104 1476
        if ($crs instanceof Geographic2D && $height !== null) {
105 9
            throw new InvalidCoordinateReferenceSystemException('A 2D geographic point must not include a height');
106
        }
107
108 1467
        if ($crs instanceof Geographic3D && $height === null) {
109 9
            throw new InvalidCoordinateReferenceSystemException('A 3D geographic point must include a height, none given');
110
        }
111
112 1458
        $this->crs = $crs;
113
114 1458
        $latitude = $this->normaliseLatitude($latitude);
115 1458
        $longitude = $this->normaliseLongitude($longitude);
116
117 1458
        $this->latitude = Angle::convert($latitude, $this->getAxisByName(Axis::GEODETIC_LATITUDE)->getUnitOfMeasureId());
118 1458
        $this->longitude = Angle::convert($longitude, $this->getAxisByName(Axis::GEODETIC_LONGITUDE)->getUnitOfMeasureId());
119
120 1458
        if ($height) {
121 180
            $this->height = Length::convert($height, $this->getAxisByName(Axis::ELLIPSOIDAL_HEIGHT)->getUnitOfMeasureId());
122
        } else {
123 1350
            $this->height = null;
124
        }
125
126 1458
        if ($epoch instanceof DateTime) {
127 9
            $epoch = DateTimeImmutable::createFromMutable($epoch);
128
        }
129 1458
        $this->epoch = $epoch;
130 1458
    }
131
132
    /**
133
     * @param Angle   $latitude  refer to CRS for preferred unit of measure, but any angle unit accepted
134
     * @param Angle   $longitude refer to CRS for preferred unit of measure, but any angle unit accepted
135
     * @param ?Length $height    refer to CRS for preferred unit of measure, but any length unit accepted
136
     */
137 1476
    public static function create(Angle $latitude, Angle $longitude, ?Length $height = null, Geographic $crs, ?DateTimeInterface $epoch = null): self
138
    {
139 1476
        return new static($latitude, $longitude, $height, $crs, $epoch);
140
    }
141
142 765
    public function getLatitude(): Angle
143
    {
144 765
        return $this->latitude;
145
    }
146
147 729
    public function getLongitude(): Angle
148
    {
149 729
        return $this->longitude;
150
    }
151
152 333
    public function getHeight(): ?Length
153
    {
154 333
        return $this->height;
155
    }
156
157 1458
    public function getCRS(): Geographic
158
    {
159 1458
        return $this->crs;
160
    }
161
162 171
    public function getCoordinateEpoch(): ?DateTimeImmutable
163
    {
164 171
        return $this->epoch;
165
    }
166
167 1458
    protected function normaliseLatitude(Angle $latitude): Angle
168
    {
169 1458
        if ($latitude->asDegrees()->getValue() > 90) {
170
            return new Degree(90);
171
        }
172 1458
        if ($latitude->asDegrees()->getValue() < -90) {
173
            return new Degree(-90);
174
        }
175
176 1458
        return $latitude;
177
    }
178
179 1458
    protected function normaliseLongitude(Angle $longitude): Angle
180
    {
181 1458
        while ($longitude->asDegrees()->getValue() > 180) {
182 9
            $longitude = $longitude->subtract(new Degree(360));
183
        }
184 1458
        while ($longitude->asDegrees()->getValue() <= -180) {
185
            $longitude = $longitude->add(new Degree(360));
186
        }
187
188 1458
        return $longitude;
189
    }
190
191
    /**
192
     * Calculate surface distance between two points.
193
     */
194 153
    public function calculateDistance(Point $to): Length
195
    {
196
        try {
197 153
            if ($to instanceof ConvertiblePoint) {
198 144
                $to = $to->convert($this->crs);
199
            }
200
        } finally {
201 153
            if ($to->getCRS()->getSRID() !== $this->crs->getSRID()) {
202 9
                throw new InvalidCoordinateReferenceSystemException('Can only calculate distances between two points in the same CRS');
203
            }
204
205
            /* @var GeographicPoint $to */
206 144
            return static::vincenty($this->asGeographicValue(), $to->asGeographicValue(), $this->getCRS()->getDatum()->getEllipsoid());
207
        }
208
    }
209
210 36
    public function __toString(): string
211
    {
212 36
        $values = [];
213 36
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
214 36
            if ($axis->getName() === Axis::GEODETIC_LATITUDE) {
215 36
                $values[] = $this->latitude;
216 36
            } elseif ($axis->getName() === Axis::GEODETIC_LONGITUDE) {
217 36
                $values[] = $this->longitude;
218 9
            } elseif ($axis->getName() === Axis::ELLIPSOIDAL_HEIGHT) {
219 9
                $values[] = $this->height;
220
            } else {
221
                throw new UnknownAxisException(); // @codeCoverageIgnore
222
            }
223
        }
224
225 36
        return '(' . implode(', ', $values) . ')';
226
    }
227
228
    /**
229
     * Geographic/geocentric conversions
230
     * In applications it is often concatenated with the 3- 7- or 10-parameter transformations 9603, 9606, 9607 or
231
     * 9636 to form a geographic to geographic transformation.
232
     */
233 9
    public function geographicGeocentric(
234
        Geocentric $to
235
    ): GeocentricPoint {
236 9
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
237 9
        $asGeocentric = $geographicValue->asGeocentricValue();
238
239 9
        return GeocentricPoint::create($asGeocentric->getX(), $asGeocentric->getY(), $asGeocentric->getZ(), $to, $this->epoch);
240
    }
241
242
    /**
243
     * Coordinate Frame rotation (geog2D/geog3D domain)
244
     * Note the analogy with the Position Vector tfm (codes 9606/1037) but beware of the differences!  The Position Vector
245
     * convention is used by IAG and recommended by ISO 19111. See methods 1032/1038/9607 for similar tfms operating
246
     * between other CRS types.
247
     */
248 63
    public function coordinateFrameRotation(
249
        Geographic $to,
250
        Length $xAxisTranslation,
251
        Length $yAxisTranslation,
252
        Length $zAxisTranslation,
253
        Angle $xAxisRotation,
254
        Angle $yAxisRotation,
255
        Angle $zAxisRotation,
256
        Scale $scaleDifference
257
    ): self {
258 63
        return $this->coordinateFrameMolodenskyBadekas(
259 63
            $to,
260
            $xAxisTranslation,
261
            $yAxisTranslation,
262
            $zAxisTranslation,
263
            $xAxisRotation,
264
            $yAxisRotation,
265
            $zAxisRotation,
266
            $scaleDifference,
267 63
            new Metre(0),
268 63
            new Metre(0),
269 63
            new Metre(0)
270
        );
271
    }
272
273
    /**
274
     * Molodensky-Badekas (CF geog2D/geog3D domain)
275
     * See method codes 1034 and 1039/9636 for this operation in other coordinate domains and method code 1062/1063 for the
276
     * opposite rotation convention in geographic 2D domain.
277
     */
278 81
    public function coordinateFrameMolodenskyBadekas(
279
        Geographic $to,
280
        Length $xAxisTranslation,
281
        Length $yAxisTranslation,
282
        Length $zAxisTranslation,
283
        Angle $xAxisRotation,
284
        Angle $yAxisRotation,
285
        Angle $zAxisRotation,
286
        Scale $scaleDifference,
287
        Length $ordinate1OfEvaluationPoint,
288
        Length $ordinate2OfEvaluationPoint,
289
        Length $ordinate3OfEvaluationPoint
290
    ): self {
291 81
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
292 81
        $asGeocentric = $geographicValue->asGeocentricValue();
293
294 81
        $xs = $asGeocentric->getX()->asMetres()->getValue();
295 81
        $ys = $asGeocentric->getY()->asMetres()->getValue();
296 81
        $zs = $asGeocentric->getZ()->asMetres()->getValue();
297 81
        $tx = $xAxisTranslation->asMetres()->getValue();
298 81
        $ty = $yAxisTranslation->asMetres()->getValue();
299 81
        $tz = $zAxisTranslation->asMetres()->getValue();
300 81
        $rx = $xAxisRotation->asRadians()->getValue();
301 81
        $ry = $yAxisRotation->asRadians()->getValue();
302 81
        $rz = $zAxisRotation->asRadians()->getValue();
303 81
        $M = 1 + $scaleDifference->asUnity()->getValue();
304 81
        $xp = $ordinate1OfEvaluationPoint->asMetres()->getValue();
305 81
        $yp = $ordinate2OfEvaluationPoint->asMetres()->getValue();
306 81
        $zp = $ordinate3OfEvaluationPoint->asMetres()->getValue();
307
308 81
        $xt = $M * ((($xs - $xp) * 1) + (($ys - $yp) * $rz) + (($zs - $zp) * -$ry)) + $tx + $xp;
309 81
        $yt = $M * ((($xs - $xp) * -$rz) + (($ys - $yp) * 1) + (($zs - $zp) * $rx)) + $ty + $yp;
310 81
        $zt = $M * ((($xs - $xp) * $ry) + (($ys - $yp) * -$rx) + (($zs - $zp) * 1)) + $tz + $zp;
311 81
        $newGeocentric = new GeocentricValue(new Metre($xt), new Metre($yt), new Metre($zt), $to->getDatum());
312 81
        $newGeographic = $newGeocentric->asGeographicValue();
313
314 81
        return static::create($newGeographic->getLatitude(), $newGeographic->getLongitude(), $to instanceof Geographic3D ? $newGeographic->getHeight() : null, $to, $this->epoch);
315
    }
316
317
    /**
318
     * Position Vector transformation (geog2D/geog3D domain)
319
     * Note the analogy with the Coordinate Frame rotation (code 9607/1038) but beware of the differences!  The Position
320
     * Vector convention is used by IAG and recommended by ISO 19111. See methods 1033/1037/9606 for similar tfms
321
     * operating between other CRS types.
322
     */
323 180
    public function positionVectorTransformation(
324
        Geographic $to,
325
        Length $xAxisTranslation,
326
        Length $yAxisTranslation,
327
        Length $zAxisTranslation,
328
        Angle $xAxisRotation,
329
        Angle $yAxisRotation,
330
        Angle $zAxisRotation,
331
        Scale $scaleDifference
332
    ): self {
333 180
        return $this->positionVectorMolodenskyBadekas(
334 180
            $to,
335
            $xAxisTranslation,
336
            $yAxisTranslation,
337
            $zAxisTranslation,
338
            $xAxisRotation,
339
            $yAxisRotation,
340
            $zAxisRotation,
341
            $scaleDifference,
342 180
            new Metre(0),
343 180
            new Metre(0),
344 180
            new Metre(0)
345
        );
346
    }
347
348
    /**
349
     * Molodensky-Badekas (PV geog2D/geog3D domain)
350
     * See method codes 1061 and 1062/1063 for this operation in other coordinate domains and method code 1039/9636 for opposite
351
     * rotation in geographic 2D/3D domain.
352
     */
353 198
    public function positionVectorMolodenskyBadekas(
354
        Geographic $to,
355
        Length $xAxisTranslation,
356
        Length $yAxisTranslation,
357
        Length $zAxisTranslation,
358
        Angle $xAxisRotation,
359
        Angle $yAxisRotation,
360
        Angle $zAxisRotation,
361
        Scale $scaleDifference,
362
        Length $ordinate1OfEvaluationPoint,
363
        Length $ordinate2OfEvaluationPoint,
364
        Length $ordinate3OfEvaluationPoint
365
    ): self {
366 198
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
367 198
        $asGeocentric = $geographicValue->asGeocentricValue();
368
369 198
        $xs = $asGeocentric->getX()->asMetres()->getValue();
370 198
        $ys = $asGeocentric->getY()->asMetres()->getValue();
371 198
        $zs = $asGeocentric->getZ()->asMetres()->getValue();
372 198
        $tx = $xAxisTranslation->asMetres()->getValue();
373 198
        $ty = $yAxisTranslation->asMetres()->getValue();
374 198
        $tz = $zAxisTranslation->asMetres()->getValue();
375 198
        $rx = $xAxisRotation->asRadians()->getValue();
376 198
        $ry = $yAxisRotation->asRadians()->getValue();
377 198
        $rz = $zAxisRotation->asRadians()->getValue();
378 198
        $M = 1 + $scaleDifference->asUnity()->getValue();
379 198
        $xp = $ordinate1OfEvaluationPoint->asMetres()->getValue();
380 198
        $yp = $ordinate2OfEvaluationPoint->asMetres()->getValue();
381 198
        $zp = $ordinate3OfEvaluationPoint->asMetres()->getValue();
382
383 198
        $xt = $M * ((($xs - $xp) * 1) + (($ys - $yp) * -$rz) + (($zs - $zp) * $ry)) + $tx + $xp;
384 198
        $yt = $M * ((($xs - $xp) * $rz) + (($ys - $yp) * 1) + (($zs - $zp) * -$rx)) + $ty + $yp;
385 198
        $zt = $M * ((($xs - $xp) * -$ry) + (($ys - $yp) * $rx) + (($zs - $zp) * 1)) + $tz + $zp;
386 198
        $newGeocentric = new GeocentricValue(new Metre($xt), new Metre($yt), new Metre($zt), $to->getDatum());
387 198
        $newGeographic = $newGeocentric->asGeographicValue();
388
389 198
        return static::create($newGeographic->getLatitude(), $newGeographic->getLongitude(), $to instanceof Geographic3D ? $newGeographic->getHeight() : null, $to, $this->epoch);
390
    }
391
392
    /**
393
     * Geocentric translations
394
     * This method allows calculation of geocentric coords in the target system by adding the parameter values to the
395
     * corresponding coordinates of the point in the source system. See methods 1031 and 1035 for similar tfms
396
     * operating between other CRSs types.
397
     */
398 72
    public function geocentricTranslation(
399
        Geographic $to,
400
        Length $xAxisTranslation,
401
        Length $yAxisTranslation,
402
        Length $zAxisTranslation
403
    ): self {
404 72
        return $this->positionVectorTransformation(
405 72
            $to,
406
            $xAxisTranslation,
407
            $yAxisTranslation,
408
            $zAxisTranslation,
409 72
            new Radian(0),
410 72
            new Radian(0),
411 72
            new Radian(0),
412 72
            new Unity(0)
413
        );
414
    }
415
416
    /**
417
     * Abridged Molodensky
418
     * This transformation is a truncated Taylor series expansion of a transformation between two geographic coordinate
419
     * systems, modelled as a set of geocentric translations.
420
     */
421 18
    public function abridgedMolodensky(
422
        Geographic $to,
423
        Length $xAxisTranslation,
424
        Length $yAxisTranslation,
425
        Length $zAxisTranslation,
426
        Length $differenceInSemiMajorAxis,
427
        Scale $differenceInFlattening
428
    ): self {
429 18
        $latitude = $this->latitude->asRadians()->getValue();
430 18
        $longitude = $this->longitude->asRadians()->getValue();
431 18
        $fromHeight = $this->height ? $this->height->asMetres()->getValue() : 0;
432 18
        $tx = $xAxisTranslation->asMetres()->getValue();
433 18
        $ty = $yAxisTranslation->asMetres()->getValue();
434 18
        $tz = $zAxisTranslation->asMetres()->getValue();
435 18
        $da = $differenceInSemiMajorAxis->asMetres()->getValue();
436 18
        $df = $differenceInFlattening->asUnity()->getValue();
437
438 18
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
439 18
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
440
441 18
        $rho = $a * (1 - $e2) / (1 - $e2 * sin($latitude) ** 2) ** (3 / 2);
442 18
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
443
444 18
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
445
446 18
        $dLatitude = ((-$tx * sin($latitude) * cos($longitude)) - ($ty * sin($latitude) * sin($longitude)) + ($tz * cos($latitude)) + ((($a * $df) + ($this->crs->getDatum()->getEllipsoid()->getInverseFlattening() * $da)) * sin(2 * $latitude))) / ($rho * sin((new ArcSecond(1))->asRadians()->getValue()));
447 18
        $dLongitude = (-$tx * sin($longitude) + $ty * cos($longitude)) / (($nu * cos($latitude)) * sin((new ArcSecond(1))->asRadians()->getValue()));
448 18
        $dHeight = ($tx * cos($latitude) * cos($longitude)) + ($ty * cos($latitude) * sin($longitude)) + ($tz * sin($latitude)) + (($a * $df + $f * $da) * (sin($latitude) ** 2)) - $da;
449
450 18
        $toLatitude = $latitude + (new ArcSecond($dLatitude))->asRadians()->getValue();
451 18
        $toLongitude = $longitude + (new ArcSecond($dLongitude))->asRadians()->getValue();
452 18
        $toHeight = $fromHeight + $dHeight;
453
454 18
        return static::create(new Radian($toLatitude), new Radian($toLongitude), $to instanceof Geographic3D ? new Metre($toHeight) : null, $to, $this->epoch);
455
    }
456
457
    /**
458
     * Molodensky
459
     * See Abridged Molodensky.
460
     */
461 18
    public function molodensky(
462
        Geographic $to,
463
        Length $xAxisTranslation,
464
        Length $yAxisTranslation,
465
        Length $zAxisTranslation,
466
        Length $differenceInSemiMajorAxis,
467
        Scale $differenceInFlattening
468
    ): self {
469 18
        $latitude = $this->latitude->asRadians()->getValue();
470 18
        $longitude = $this->longitude->asRadians()->getValue();
471 18
        $fromHeight = $this->height ? $this->height->asMetres()->getValue() : 0;
472 18
        $tx = $xAxisTranslation->asMetres()->getValue();
473 18
        $ty = $yAxisTranslation->asMetres()->getValue();
474 18
        $tz = $zAxisTranslation->asMetres()->getValue();
475 18
        $da = $differenceInSemiMajorAxis->asMetres()->getValue();
476 18
        $df = $differenceInFlattening->asUnity()->getValue();
477
478 18
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
479 18
        $b = $this->crs->getDatum()->getEllipsoid()->getSemiMinorAxis()->asMetres()->getValue();
480 18
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
481
482 18
        $rho = $a * (1 - $e2) / (1 - $e2 * sin($latitude) ** 2) ** (3 / 2);
483 18
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
484
485 18
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
0 ignored issues
show
Unused Code introduced by
The assignment to $f is dead and can be removed.
Loading history...
486
487 18
        $dLatitude = ((-$tx * sin($latitude) * cos($longitude)) - ($ty * sin($latitude) * sin($longitude)) + ($tz * cos($latitude)) + ($da * ($nu * $e2 * sin($latitude) * cos($latitude)) / $a + $df * ($rho * ($a / $b) + $nu * ($b / $a)) * sin($latitude) * cos($latitude))) / (($rho + $fromHeight) * sin((new ArcSecond(1))->asRadians()->getValue()));
488 18
        $dLongitude = (-$tx * sin($longitude) + $ty * cos($longitude)) / ((($nu + $fromHeight) * cos($latitude)) * sin((new ArcSecond(1))->asRadians()->getValue()));
489 18
        $dHeight = ($tx * cos($latitude) * cos($longitude)) + ($ty * cos($latitude) * sin($longitude)) + ($tz * sin($latitude)) - $da * $a / $nu + $df * $b / $a * $nu * sin($latitude) ** 2;
490
491 18
        $toLatitude = $latitude + (new ArcSecond($dLatitude))->asRadians()->getValue();
492 18
        $toLongitude = $longitude + (new ArcSecond($dLongitude))->asRadians()->getValue();
493 18
        $toHeight = $fromHeight + $dHeight;
494
495 18
        return static::create(new Radian($toLatitude), new Radian($toLongitude), $to instanceof Geographic3D ? new Metre($toHeight) : null, $to, $this->epoch);
496
    }
497
498
    /**
499
     * Albers Equal Area.
500
     */
501 18
    public function albersEqualArea(
502
        Projected $to,
503
        Angle $latitudeOfFalseOrigin,
504
        Angle $longitudeOfFalseOrigin,
505
        Angle $latitudeOf1stStandardParallel,
506
        Angle $latitudeOf2ndStandardParallel,
507
        Length $eastingAtFalseOrigin,
508
        Length $northingAtFalseOrigin
509
    ): ProjectedPoint {
510 18
        $latitude = $this->latitude->asRadians()->getValue();
511 18
        $longitude = $this->longitude->asRadians()->getValue();
0 ignored issues
show
Unused Code introduced by
The assignment to $longitude is dead and can be removed.
Loading history...
512 18
        $phiOrigin = $latitudeOfFalseOrigin->asRadians()->getValue();
513 18
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
514 18
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
515 18
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
516 18
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
517 18
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
518
519 18
        $centralMeridianFirstParallel = cos($phi1) / sqrt(1 - ($e2 * sin($phi1) ** 2));
520 18
        $centralMeridianSecondParallel = cos($phi2) / sqrt(1 - ($e2 * sin($phi2) ** 2));
521
522 18
        $alpha = (1 - $e2) * (sin($latitude) / (1 - $e2 * sin($latitude) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))));
523 18
        $alphaOrigin = (1 - $e2) * (sin($phiOrigin) / (1 - $e2 * sin($phiOrigin) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phiOrigin)) / (1 + $e * sin($phiOrigin))));
524 18
        $alphaFirstParallel = (1 - $e2) * (sin($phi1) / (1 - $e2 * sin($phi1) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))));
525 18
        $alphaSecondParallel = (1 - $e2) * (sin($phi2) / (1 - $e2 * sin($phi2) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))));
526
527 18
        $n = ($centralMeridianFirstParallel ** 2 - $centralMeridianSecondParallel ** 2) / ($alphaSecondParallel - $alphaFirstParallel);
528 18
        $C = $centralMeridianFirstParallel ** 2 + $n * $alphaFirstParallel;
529 18
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfFalseOrigin))->asRadians()->getValue();
530 18
        $rho = $a * sqrt($C - $n * $alpha) / $n;
531 18
        $rhoOrigin = ($a * sqrt($C - $n * $alphaOrigin)) / $n;
532
533 18
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + ($rho * sin($theta));
534 18
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rhoOrigin - ($rho * cos($theta));
535
536 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
537
    }
538
539
    /**
540
     * American Polyconic.
541
     */
542 9
    public function americanPolyconic(
543
        Projected $to,
544
        Angle $latitudeOfNaturalOrigin,
545
        Angle $longitudeOfNaturalOrigin,
546
        Length $falseEasting,
547
        Length $falseNorthing
548
    ): ProjectedPoint {
549 9
        $latitude = $this->latitude->asRadians()->getValue();
550 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
551 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
552 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
553 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
554 9
        $e4 = $e ** 4;
555 9
        $e6 = $e ** 6;
556
557 9
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
558 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));
559
560 9
        if ($latitude === 0.0) {
0 ignored issues
show
introduced by
The condition $latitude === 0.0 is always false.
Loading history...
561
            $easting = $falseEasting->asMetres()->getValue() + $a * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
562
            $northing = $falseNorthing->asMetres()->getValue() - $MO;
563
        } else {
564 9
            $L = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * sin($latitude);
565 9
            $nu = $a / sqrt(1 - $e2 * sin($latitude) ** 2);
566
567 9
            $easting = $falseEasting->asMetres()->getValue() + $nu * 1 / tan($latitude) * sin($L);
568 9
            $northing = $falseNorthing->asMetres()->getValue() + $M - $MO + $nu * 1 / tan($latitude) * (1 - cos($L));
569
        }
570
571 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
572
    }
573
574
    /**
575
     * Bonne.
576
     */
577 9
    public function bonne(
578
        Projected $to,
579
        Angle $latitudeOfNaturalOrigin,
580
        Angle $longitudeOfNaturalOrigin,
581
        Length $falseEasting,
582
        Length $falseNorthing
583
    ): ProjectedPoint {
584 9
        $latitude = $this->latitude->asRadians()->getValue();
585 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
586 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
587 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
588 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
589 9
        $e4 = $e ** 4;
590 9
        $e6 = $e ** 6;
591
592 9
        $m = cos($latitude) / sqrt(1 - $e2 * sin($latitude) ** 2);
593 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
594
595 9
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
596 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));
597
598 9
        $rho = $a * $mO / sin($latitudeOrigin) + $MO - $M;
599 9
        $tau = $a * $m * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() / $rho;
600
601 9
        $easting = $falseEasting->asMetres()->getValue() + ($rho * sin($tau));
602 9
        $northing = $falseNorthing->asMetres()->getValue() + (($a * $mO / sin($latitudeOrigin) - $rho * cos($tau)));
603
604 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
605
    }
606
607
    /**
608
     * Bonne South Orientated.
609
     */
610 9
    public function bonneSouthOrientated(
611
        Projected $to,
612
        Angle $latitudeOfNaturalOrigin,
613
        Angle $longitudeOfNaturalOrigin,
614
        Length $falseEasting,
615
        Length $falseNorthing
616
    ): ProjectedPoint {
617 9
        $latitude = $this->latitude->asRadians()->getValue();
618 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
619 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
620 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
621 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
622 9
        $e4 = $e ** 4;
623 9
        $e6 = $e ** 6;
624
625 9
        $m = cos($latitude) / sqrt(1 - $e2 * sin($latitude) ** 2);
626 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
627
628 9
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
629 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));
630
631 9
        $rho = $a * $mO / sin($latitudeOrigin) + $MO - $M;
632 9
        $tau = $a * $m * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() / $rho;
633
634 9
        $westing = $falseEasting->asMetres()->getValue() - ($rho * sin($tau));
635 9
        $southing = $falseNorthing->asMetres()->getValue() - (($a * $mO / sin($latitudeOrigin) - $rho * cos($tau)));
636
637 9
        return ProjectedPoint::create(new Metre(-$westing), new Metre(-$southing), new Metre($westing), new Metre($southing), $to, $this->epoch);
638
    }
639
640
    /**
641
     * Cassini-Soldner.
642
     */
643 9
    public function cassiniSoldner(
644
        Projected $to,
645
        Angle $latitudeOfNaturalOrigin,
646
        Angle $longitudeOfNaturalOrigin,
647
        Length $falseEasting,
648
        Length $falseNorthing
649
    ): ProjectedPoint {
650 9
        $latitude = $this->latitude->asRadians()->getValue();
651 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
652 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
653 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
654 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
655 9
        $e4 = $e ** 4;
656 9
        $e6 = $e ** 6;
657
658 9
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
659 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));
660
661 9
        $A = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * cos($latitude);
662 9
        $T = tan($latitude) ** 2;
663 9
        $C = $e2 * cos($latitude) ** 2 / (1 - $e2);
664 9
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
665 9
        $X = $M - $MO + $nu * tan($latitude) * ($A ** 2 / 2 + (5 - $T + 6 * $C) * $A ** 4 / 24);
666
667 9
        $easting = $falseEasting->asMetres()->getValue() + $nu * ($A - $T * $A ** 3 / 6 - (8 - $T + 8 * $C) * $T * $A ** 5 / 120);
668 9
        $northing = $falseNorthing->asMetres()->getValue() + $X;
669
670 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
671
    }
672
673
    /**
674
     * Hyperbolic Cassini-Soldner.
675
     */
676 18
    public function hyperbolicCassiniSoldner(
677
        Projected $to,
678
        Angle $latitudeOfNaturalOrigin,
679
        Angle $longitudeOfNaturalOrigin,
680
        Length $falseEasting,
681
        Length $falseNorthing
682
    ): ProjectedPoint {
683 18
        $latitude = $this->latitude->asRadians()->getValue();
684 18
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
685 18
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
686 18
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
687 18
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
688 18
        $e4 = $e ** 4;
689 18
        $e6 = $e ** 6;
690
691 18
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
692 18
        $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));
693
694 18
        $A = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * cos($latitude);
695 18
        $T = tan($latitude) ** 2;
696 18
        $C = $e2 * cos($latitude) ** 2 / (1 - $e2);
697 18
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
698 18
        $rho = $a * (1 - $e2) / (1 - $e2 * sin($latitude) ** 2) ** (3 / 2);
699 18
        $X = $M - $MO + $nu * tan($latitude) * ($A ** 2 / 2 + (5 - $T + 6 * $C) * $A ** 4 / 24);
700
701 18
        $easting = $falseEasting->asMetres()->getValue() + $nu * ($A - $T * $A ** 3 / 6 - (8 - $T + 8 * $C) * $T * $A ** 5 / 120);
702 18
        $northing = $falseNorthing->asMetres()->getValue() + $X - ($X ** 3 / (6 * $rho * $nu));
703
704 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
705
    }
706
707
    /**
708
     * Colombia Urban.
709
     */
710 9
    public function columbiaUrban(
711
        Projected $to,
712
        Angle $latitudeOfNaturalOrigin,
713
        Angle $longitudeOfNaturalOrigin,
714
        Length $falseEasting,
715
        Length $falseNorthing,
716
        Length $projectionPlaneOriginHeight
717
    ): ProjectedPoint {
718 9
        $latitude = $this->latitude->asRadians()->getValue();
719 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
720 9
        $heightOrigin = $projectionPlaneOriginHeight->asMetres()->getValue();
721 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
722 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
723
724 9
        $rhoOrigin = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** (3 / 2);
725 9
        $rhoMid = $a * (1 - $e2) / (1 - $e2 * sin(($latitude + $latitudeOrigin) / 2) ** 2) ** (3 / 2);
726
727 9
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
728 9
        $nuOrigin = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
729
730 9
        $A = 1 + $heightOrigin / $nuOrigin;
731 9
        $B = tan($latitudeOrigin) / (2 * $rhoOrigin * $nuOrigin);
732 9
        $G = 1 + $heightOrigin / $rhoMid;
733
734 9
        $easting = $falseEasting->asMetres()->getValue() + $A * $nu * cos($latitude) * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
735 9
        $northing = $falseNorthing->asMetres()->getValue() + $G * $rhoOrigin * (($latitude - $latitudeOrigin) + ($B * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() ** 2 * $nu ** 2 * cos($latitude) ** 2));
736
737 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
738
    }
739
740
    /**
741
     * Equal Earth.
742
     */
743 9
    public function equalEarth(
744
        Projected $to,
745
        Angle $longitudeOfNaturalOrigin,
746
        Length $falseEasting,
747
        Length $falseNorthing
748
    ): ProjectedPoint {
749 9
        $latitude = $this->latitude->asRadians()->getValue();
750 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
751 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
752 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
753
754 9
        $q = (1 - $e2) * ((sin($latitude) / (1 - $e2 * sin($latitude) ** 2)) - (1 / (2 * $e) * log((1 - $e * sin($latitude)) / (1 + $e * sin($latitude)))));
755 9
        $qP = (1 - $e2) * ((1 / (1 - $e2)) - (1 / (2 * $e) * log((1 - $e) / (1 + $e))));
756 9
        $beta = self::asin($q / $qP);
757 9
        $theta = self::asin(sin($beta) * sqrt(3) / 2);
758 9
        $Rq = $a * sqrt($qP / 2);
759
760 9
        $easting = $falseEasting->asMetres()->getValue() + ($Rq * 2 * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * cos($theta)) / (sqrt(3) * (1.340264 - 0.243318 * $theta ** 2 + $theta ** 6 * (0.006251 + 0.034164 * $theta ** 2)));
761 9
        $northing = $falseNorthing->asMetres()->getValue() + $Rq * $theta * (1.340264 - 0.081106 * $theta ** 2 + $theta ** 6 * (0.000893 + 0.003796 * $theta ** 2));
762
763 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
764
    }
765
766
    /**
767
     * Equidistant Cylindrical
768
     * See method code 1029 for spherical development. See also Pseudo Plate Carree, method code 9825.
769
     */
770 9
    public function equidistantCylindrical(
771
        Projected $to,
772
        Angle $latitudeOf1stStandardParallel,
773
        Angle $longitudeOfNaturalOrigin,
774
        Length $falseEasting,
775
        Length $falseNorthing
776
    ): ProjectedPoint {
777 9
        $latitude = $this->latitude->asRadians()->getValue();
778 9
        $latitudeFirstParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
779 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
780 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
781 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
782 9
        $e4 = $e ** 4;
783 9
        $e6 = $e ** 6;
784 9
        $e8 = $e ** 8;
785 9
        $e10 = $e ** 10;
786 9
        $e12 = $e ** 12;
787 9
        $e14 = $e ** 14;
788
789 9
        $nu1 = $a / sqrt(1 - $e2 * sin($latitudeFirstParallel) ** 2);
790
791
        $M = $a * (
792 9
            (1 - 1 / 4 * $e2 - 3 / 64 * $e4 - 5 / 256 * $e6 - 175 / 16384 * $e8 - 441 / 65536 * $e10 - 4851 / 1048576 * $e12 - 14157 / 4194304 * $e14) * $latitude +
793 9
            (-3 / 8 * $e2 - 3 / 32 * $e4 - 45 / 1024 * $e6 - 105 / 4096 * $e8 - 2205 / 131072 * $e10 - 6237 / 524288 * $e12 - 297297 / 33554432 * $e14) * sin(2 * $latitude) +
794 9
            (15 / 256 * $e4 + 45 / 1024 * $e ** 6 + 525 / 16384 * $e ** 8 + 1575 / 65536 * $e10 + 155925 / 8388608 * $e12 + 495495 / 33554432 * $e14) * sin(4 * $latitude) +
795 9
            (-35 / 3072 * $e6 - 175 / 12288 * $e8 - 3675 / 262144 * $e10 - 13475 / 1048576 * $e12 - 385385 / 33554432 * $e14) * sin(6 * $latitude) +
796 9
            (315 / 131072 * $e8 + 2205 / 524288 * $e10 + 43659 / 8388608 * $e12 + 189189 / 33554432 * $e14) * sin(8 * $latitude) +
797 9
            (-693 / 1310720 * $e10 - 6537 / 5242880 * $e12 - 297297 / 167772160 * $e14) * sin(10 * $latitude) +
798 9
            (1001 / 8388608 * $e12 + 11011 / 33554432 * $e14) * sin(12 * $latitude) +
799 9
            (-6435 / 234881024 * $e ** 14) * sin(14 * $latitude)
800
        );
801
802 9
        $easting = $falseEasting->asMetres()->getValue() + $nu1 * cos($latitudeFirstParallel) * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
803 9
        $northing = $falseNorthing->asMetres()->getValue() + $M;
804
805 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
806
    }
807
808
    /**
809
     * Guam Projection
810
     * Simplified form of Oblique Azimuthal Equidistant projection method.
811
     */
812 9
    public function guamProjection(
813
        Projected $to,
814
        Angle $latitudeOfNaturalOrigin,
815
        Angle $longitudeOfNaturalOrigin,
816
        Length $falseEasting,
817
        Length $falseNorthing
818
    ): ProjectedPoint {
819 9
        $latitude = $this->latitude->asRadians()->getValue();
820 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
821 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
822 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
823 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
824 9
        $e4 = $e ** 4;
825 9
        $e6 = $e ** 6;
826
827 9
        $M = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitude - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitude) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitude) - (35 * $e6 / 3072) * sin(6 * $latitude));
828 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));
829 9
        $x = ($a * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * cos($latitude)) / sqrt(1 - $e2 * sin($latitude) ** 2);
830
831 9
        $easting = $falseEasting->asMetres()->getValue() + $x;
832 9
        $northing = $falseNorthing->asMetres()->getValue() + $M - $MO + ($x ** 2 * tan($latitude) * sqrt(1 - $e2 * sin($latitude) ** 2) / (2 * $a));
833
834 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
835
    }
836
837
    /**
838
     * Krovak.
839
     */
840 36
    public function krovak(
841
        Projected $to,
842
        Angle $latitudeOfProjectionCentre,
843
        Angle $longitudeOfOrigin,
844
        Angle $coLatitudeOfConeAxis,
845
        Angle $latitudeOfPseudoStandardParallel,
846
        Scale $scaleFactorOnPseudoStandardParallel,
847
        Length $falseEasting,
848
        Length $falseNorthing
849
    ): ProjectedPoint {
850 36
        $longitudeOffset = $to->getDatum()->getPrimeMeridian()->getGreenwichLongitude()->asRadians()->getValue() - $this->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()->asRadians()->getValue();
851 36
        $latitude = $this->latitude->asRadians()->getValue();
852 36
        $longitude = $this->longitude->asRadians()->getValue() - $longitudeOffset;
853 36
        $latitudeC = $latitudeOfProjectionCentre->asRadians()->getValue();
854 36
        $longitudeO = $longitudeOfOrigin->asRadians()->getValue();
855 36
        $alphaC = $coLatitudeOfConeAxis->asRadians()->getValue();
856 36
        $latitudeP = $latitudeOfPseudoStandardParallel->asRadians()->getValue();
857 36
        $kP = $scaleFactorOnPseudoStandardParallel->asUnity()->getValue();
858 36
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
859 36
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
860 36
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
861
862 36
        $A = $a * sqrt(1 - $e2) / (1 - $e2 * sin($latitudeC) ** 2);
863 36
        $B = sqrt(1 + $e2 * cos($latitudeC) ** 4 / (1 - $e2));
864 36
        $upsilonO = self::asin(sin($latitudeC) / $B);
865 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);
866 36
        $n = sin($latitudeP);
867 36
        $rO = $kP * $A / tan($latitudeP);
868
869 36
        $U = 2 * (atan($tO * tan($latitude / 2 + M_PI / 4) ** $B / ((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e * $B / 2)) - M_PI / 4);
870 36
        $V = $B * ($longitudeO - $longitude);
871 36
        $T = self::asin(cos($alphaC) * sin($U) + sin($alphaC) * cos($U) * cos($V));
872 36
        $D = atan2(cos($U) * sin($V) / cos($T), ((cos($alphaC) * sin($T) - sin($U)) / (sin($alphaC) * cos($T))));
873 36
        $theta = $n * $D;
874 36
        $r = $rO * tan(M_PI / 4 + $latitudeP / 2) ** $n / tan($T / 2 + M_PI / 4) ** $n;
875 36
        $X = $r * cos($theta);
876 36
        $Y = $r * sin($theta);
877
878 36
        $westing = $Y + $falseEasting->asMetres()->getValue();
879 36
        $southing = $X + $falseNorthing->asMetres()->getValue();
880
881 36
        return ProjectedPoint::create(new Metre(-$westing), new Metre(-$southing), new Metre($westing), new Metre($southing), $to, $this->epoch);
882
    }
883
884
    /**
885
     * Krovak Modified
886
     * Incorporates a polynomial transformation which is defined to be exact and for practical purposes is considered
887
     * to be a map projection.
888
     */
889 18
    public function krovakModified(
890
        Projected $to,
891
        Angle $latitudeOfProjectionCentre,
892
        Angle $longitudeOfOrigin,
893
        Angle $coLatitudeOfConeAxis,
894
        Angle $latitudeOfPseudoStandardParallel,
895
        Scale $scaleFactorOnPseudoStandardParallel,
896
        Length $falseEasting,
897
        Length $falseNorthing,
898
        Length $ordinate1OfEvaluationPoint,
899
        Length $ordinate2OfEvaluationPoint,
900
        Coefficient $C1,
901
        Coefficient $C2,
902
        Coefficient $C3,
903
        Coefficient $C4,
904
        Coefficient $C5,
905
        Coefficient $C6,
906
        Coefficient $C7,
907
        Coefficient $C8,
908
        Coefficient $C9,
909
        Coefficient $C10
910
    ): ProjectedPoint {
911 18
        $asKrovak = $this->krovak($to, $latitudeOfProjectionCentre, $longitudeOfOrigin, $coLatitudeOfConeAxis, $latitudeOfPseudoStandardParallel, $scaleFactorOnPseudoStandardParallel, new Metre(0), new Metre(0));
912
913 18
        $westing = $asKrovak->getWesting()->asMetres()->getValue();
914 18
        $southing = $asKrovak->getSouthing()->asMetres()->getValue();
915 18
        $c1 = $C1->asUnity()->getValue();
916 18
        $c2 = $C2->asUnity()->getValue();
917 18
        $c3 = $C3->asUnity()->getValue();
918 18
        $c4 = $C4->asUnity()->getValue();
919 18
        $c5 = $C5->asUnity()->getValue();
920 18
        $c6 = $C6->asUnity()->getValue();
921 18
        $c7 = $C7->asUnity()->getValue();
922 18
        $c8 = $C8->asUnity()->getValue();
923 18
        $c9 = $C9->asUnity()->getValue();
924 18
        $c10 = $C10->asUnity()->getValue();
925
926 18
        $Xr = $southing - $ordinate1OfEvaluationPoint->asMetres()->getValue();
927 18
        $Yr = $westing - $ordinate2OfEvaluationPoint->asMetres()->getValue();
928
929 18
        $dX = $c1 + $c3 * $Xr - $c4 * $Yr - 2 * $c6 * $Xr * $Yr + $c5 * ($Xr ** 2 - $Yr ** 2) + $c7 * $Xr * ($Xr ** 2 - 3 * $Yr ** 2) - $c8 * $Yr * (3 * $Xr ** 2 - $Yr ** 2) + 4 * $c9 * $Xr * $Yr * ($Xr ** 2 - $Yr ** 2) + $c10 * ($Xr ** 4 + $Yr ** 4 - 6 * $Xr ** 2 * $Yr ** 2);
930 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);
931
932 18
        $westing += $falseEasting->asMetres()->getValue() - $dY;
933 18
        $southing += $falseNorthing->asMetres()->getValue() - $dX;
934
935 18
        return ProjectedPoint::create(new Metre(-$westing), new Metre(-$southing), new Metre($westing), new Metre($southing), $to, $this->epoch);
936
    }
937
938
    /**
939
     * Lambert Azimuthal Equal Area
940
     * This is the ellipsoidal form of the projection.
941
     */
942 9
    public function lambertAzimuthalEqualArea(
943
        Projected $to,
944
        Angle $latitudeOfNaturalOrigin,
945
        Angle $longitudeOfNaturalOrigin,
946
        Length $falseEasting,
947
        Length $falseNorthing
948
    ): ProjectedPoint {
949 9
        $latitude = $this->latitude->asRadians()->getValue();
950 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
951 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
952 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
953 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
954
955 9
        $q = (1 - $e2) * ((sin($latitude) / (1 - $e2 * sin($latitude) ** 2)) - ((1 / (2 * $e)) * log((1 - $e * sin($latitude)) / (1 + $e * sin($latitude)))));
956 9
        $qO = (1 - $e2) * ((sin($latitudeOrigin) / (1 - $e2 * sin($latitudeOrigin) ** 2)) - ((1 / (2 * $e)) * log((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin)))));
957 9
        $qP = (1 - $e2) * ((1 / (1 - $e2)) - ((1 / (2 * $e)) * log((1 - $e) / (1 + $e))));
958 9
        $beta = self::asin($q / $qP);
959 9
        $betaO = self::asin($qO / $qP);
960 9
        $Rq = $a * sqrt($qP / 2);
961 9
        $B = $Rq * sqrt(2 / (1 + sin($betaO) * sin($beta) + (cos($betaO) * cos($beta) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()))));
962 9
        $D = $a * (cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2)) / ($Rq * cos($betaO));
963
964 9
        $easting = $falseEasting->asMetres()->getValue() + (($B * $D) * (cos($beta) * sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
965 9
        $northing = $falseNorthing->asMetres()->getValue() + ($B / $D) * ((cos($betaO) * sin($beta)) - (sin($betaO) * cos($beta) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
966
967 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
968
    }
969
970
    /**
971
     * Lambert Azimuthal Equal Area (Spherical)
972
     * This is the spherical form of the projection.  See coordinate operation method Lambert Azimuthal Equal Area
973
     * (code 9820) for ellipsoidal form.  Differences of several tens of metres result from comparison of the two
974
     * methods.
975
     */
976 9
    public function lambertAzimuthalEqualAreaSpherical(
977
        Projected $to,
978
        Angle $latitudeOfNaturalOrigin,
979
        Angle $longitudeOfNaturalOrigin,
980
        Length $falseEasting,
981
        Length $falseNorthing
982
    ): ProjectedPoint {
983 9
        $latitude = $this->latitude->asRadians()->getValue();
984 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
985 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
986
987 9
        $k = sqrt(2 / (1 + sin($latitudeOrigin) * sin($latitude) + cos($latitudeOrigin) * cos($latitude) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
988
989 9
        $easting = $falseEasting->asMetres()->getValue() + ($a * $k * cos($latitude) * sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()));
990 9
        $northing = $falseNorthing->asMetres()->getValue() + ($a * $k * (cos($latitudeOrigin) * sin($latitude) - sin($latitudeOrigin) * cos($latitude) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
991
992 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
993
    }
994
995
    /**
996
     * Lambert Conic Conformal (1SP).
997
     */
998 9
    public function lambertConicConformal1SP(
999
        Projected $to,
1000
        Angle $latitudeOfNaturalOrigin,
1001
        Angle $longitudeOfNaturalOrigin,
1002
        Scale $scaleFactorAtNaturalOrigin,
1003
        Length $falseEasting,
1004
        Length $falseNorthing
1005
    ): ProjectedPoint {
1006 9
        $latitude = $this->latitude->asRadians()->getValue();
1007 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1008 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1009 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1010 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1011 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1012
1013 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
1014 9
        $tO = tan(M_PI / 4 - $latitudeOrigin / 2) / ((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin))) ** ($e / 2);
1015 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1016 9
        $n = sin($latitudeOrigin);
1017 9
        $F = $mO / ($n * $tO ** $n);
1018 9
        $rO = $a * $F * $tO ** $n * $kO;
1019 9
        $r = $a * $F * $t ** $n * $kO;
1020 9
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1021
1022 9
        $easting = $falseEasting->asMetres()->getValue() + $r * sin($theta);
1023 9
        $northing = $falseNorthing->asMetres()->getValue() + $rO - $r * cos($theta);
1024
1025 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1026
    }
1027
1028
    /**
1029
     * Lambert Conic Conformal (1SP) Variant B.
1030
     */
1031
    public function lambertConicConformal1SPVariantB(
1032
        Projected $to,
1033
        Angle $latitudeOfNaturalOrigin,
1034
        Scale $scaleFactorAtNaturalOrigin,
1035
        Angle $latitudeOfFalseOrigin,
1036
        Angle $longitudeOfFalseOrigin,
1037
        Length $eastingAtFalseOrigin,
1038
        Length $northingAtFalseOrigin
1039
    ): ProjectedPoint {
1040
        $latitude = $this->latitude->asRadians()->getValue();
1041
        $latitudeNaturalOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1042
        $latitudeFalseOrigin = $latitudeOfFalseOrigin->asRadians()->getValue();
1043
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1044
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1045
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1046
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1047
1048
        $mO = cos($latitudeNaturalOrigin) / sqrt(1 - $e2 * sin($latitudeNaturalOrigin) ** 2);
1049
        $tO = tan(M_PI / 4 - $latitudeNaturalOrigin / 2) / ((1 - $e * sin($latitudeNaturalOrigin)) / (1 + $e * sin($latitudeNaturalOrigin))) ** ($e / 2);
1050
        $tF = tan(M_PI / 4 - $latitudeFalseOrigin / 2) / ((1 - $e * sin($latitudeFalseOrigin)) / (1 + $e * sin($latitudeFalseOrigin))) ** ($e / 2);
1051
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1052
        $n = sin($latitudeNaturalOrigin);
1053
        $F = $mO / ($n * $tO ** $n);
1054
        $rF = $a * $F * $tF ** $n * $kO;
1055
        $r = $a * $F * $t ** $n * $kO;
1056
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfFalseOrigin))->asRadians()->getValue();
1057
1058
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $r * sin($theta);
1059
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1060
1061
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1062
    }
1063
1064
    /**
1065
     * Lambert Conic Conformal (2SP Belgium)
1066
     * In 2000 this modification was replaced through use of the regular Lambert Conic Conformal (2SP) method [9802]
1067
     * with appropriately modified parameter values.
1068
     */
1069 9
    public function lambertConicConformal2SPBelgium(
1070
        Projected $to,
1071
        Angle $latitudeOfFalseOrigin,
1072
        Angle $longitudeOfFalseOrigin,
1073
        Angle $latitudeOf1stStandardParallel,
1074
        Angle $latitudeOf2ndStandardParallel,
1075
        Length $eastingAtFalseOrigin,
1076
        Length $northingAtFalseOrigin
1077
    ): ProjectedPoint {
1078 9
        $latitude = $this->latitude->asRadians()->getValue();
1079 9
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1080 9
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1081 9
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1082 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1083 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1084 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1085
1086 9
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1087 9
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1088 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1089 9
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1090 9
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1091 9
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1092 9
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1093 9
        $F = $m1 / ($n * $t1 ** $n);
1094 9
        $r = $a * $F * $t ** $n;
1095 9
        $rF = $a * $F * $tF ** $n;
1096 9
        if (is_nan($rF)) {
1097 9
            $rF = 0;
1098
        }
1099 9
        $theta = ($n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfFalseOrigin))->asRadians()->getValue()) - (new ArcSecond(29.2985))->asRadians()->getValue();
1100
1101 9
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $r * sin($theta);
1102 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1103
1104 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1105
    }
1106
1107
    /**
1108
     * Lambert Conic Conformal (2SP Michigan).
1109
     */
1110 9
    public function lambertConicConformal2SPMichigan(
1111
        Projected $to,
1112
        Angle $latitudeOfFalseOrigin,
1113
        Angle $longitudeOfFalseOrigin,
1114
        Angle $latitudeOf1stStandardParallel,
1115
        Angle $latitudeOf2ndStandardParallel,
1116
        Length $eastingAtFalseOrigin,
1117
        Length $northingAtFalseOrigin,
1118
        Scale $ellipsoidScalingFactor
1119
    ): ProjectedPoint {
1120 9
        $latitude = $this->latitude->asRadians()->getValue();
1121 9
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1122 9
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1123 9
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1124 9
        $K = $ellipsoidScalingFactor->asUnity()->getValue();
1125 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1126 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1127 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1128
1129 9
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1130 9
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1131 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1132 9
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1133 9
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1134 9
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1135 9
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1136 9
        $F = $m1 / ($n * $t1 ** $n);
1137 9
        $r = $a * $K * $F * $t ** $n;
1138 9
        $rF = $a * $K * $F * $tF ** $n;
1139 9
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfFalseOrigin))->asRadians()->getValue();
1140
1141 9
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $r * sin($theta);
1142 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1143
1144 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1145
    }
1146
1147
    /**
1148
     * Lambert Conic Conformal (2SP).
1149
     */
1150 9
    public function lambertConicConformal2SP(
1151
        Projected $to,
1152
        Angle $latitudeOfFalseOrigin,
1153
        Angle $longitudeOfFalseOrigin,
1154
        Angle $latitudeOf1stStandardParallel,
1155
        Angle $latitudeOf2ndStandardParallel,
1156
        Length $eastingAtFalseOrigin,
1157
        Length $northingAtFalseOrigin
1158
    ): ProjectedPoint {
1159 9
        $latitude = $this->latitude->asRadians()->getValue();
1160 9
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1161 9
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1162 9
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1163 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1164 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1165 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1166
1167 9
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1168 9
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1169 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1170 9
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1171 9
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1172 9
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1173 9
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1174 9
        $F = $m1 / ($n * $t1 ** $n);
1175 9
        $r = $a * $F * $t ** $n;
1176 9
        $rF = $a * $F * $tF ** $n;
1177 9
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfFalseOrigin))->asRadians()->getValue();
1178
1179 9
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $r * sin($theta);
1180 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1181
1182 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1183
    }
1184
1185
    /**
1186
     * Lambert Conic Conformal (West Orientated).
1187
     */
1188
    public function lambertConicConformalWestOrientated(
1189
        Projected $to,
1190
        Angle $latitudeOfNaturalOrigin,
1191
        Angle $longitudeOfNaturalOrigin,
1192
        Scale $scaleFactorAtNaturalOrigin,
1193
        Length $falseEasting,
1194
        Length $falseNorthing
1195
    ): ProjectedPoint {
1196
        $latitude = $this->latitude->asRadians()->getValue();
1197
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1198
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1199
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1200
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1201
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1202
1203
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
1204
        $tO = tan(M_PI / 4 - $latitudeOrigin / 2) / ((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin))) ** ($e / 2);
1205
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1206
        $n = sin($latitudeOrigin);
1207
        $F = $mO / ($n * $tO ** $n);
1208
        $rO = $a * $F * $tO ** $n ** $kO;
1209
        $r = $a * $F * $t ** $n ** $kO;
1210
        $theta = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1211
1212
        $westing = $falseEasting->asMetres()->getValue() - $r * sin($theta);
1213
        $northing = $falseNorthing->asMetres()->getValue() + $rO - $r * cos($theta);
1214
1215
        return ProjectedPoint::create(new Metre(-$westing), new Metre($northing), new Metre($westing), new Metre(-$northing), $to, $this->epoch);
1216
    }
1217
1218
    /**
1219
     * Lambert Conic Near-Conformal
1220
     * The Lambert Near-Conformal projection is derived from the Lambert Conformal Conic projection by truncating the
1221
     * series expansion of the projection formulae.
1222
     */
1223 9
    public function lambertConicNearConformal(
1224
        Projected $to,
1225
        Angle $latitudeOfNaturalOrigin,
1226
        Angle $longitudeOfNaturalOrigin,
1227
        Scale $scaleFactorAtNaturalOrigin,
1228
        Length $falseEasting,
1229
        Length $falseNorthing
1230
    ): ProjectedPoint {
1231 9
        $latitude = $this->latitude->asRadians()->getValue();
1232 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1233 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1234 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1235 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1236 9
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
1237
1238 9
        $n = $f / (2 - $f);
1239 9
        $rhoO = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** (3 / 2);
1240 9
        $nuO = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
1241 9
        $A = 1 / (6 * $rhoO * $nuO);
1242 9
        $APrime = $a * (1 - $n + 5 * ($n ** 2 - $n ** 3) / 4 + 81 * ($n ** 4 - $n ** 5) / 64);
1243 9
        $BPrime = 3 * $a * ($n - $n ** 2 + 7 * ($n ** 3 - $n ** 4) / 8 + 55 * $n ** 5 / 64) / 2;
1244 9
        $CPrime = 15 * $a * ($n ** 2 - $n ** 3 + 3 * ($n ** 4 - $n ** 5) / 4) / 16;
1245 9
        $DPrime = 35 * $a * ($n ** 3 - $n ** 4 + 11 * $n ** 5 / 16) / 48;
1246 9
        $EPrime = 315 * $a * ($n ** 4 - $n ** 5) / 512;
1247 9
        $rO = $kO * $nuO / tan($latitudeOrigin);
1248 9
        $sO = $APrime * $latitudeOrigin - $BPrime * sin(2 * $latitudeOrigin) + $CPrime * sin(4 * $latitudeOrigin) - $DPrime * sin(6 * $latitudeOrigin) + $EPrime * sin(8 * $latitudeOrigin);
1249 9
        $s = $APrime * $latitude - $BPrime * sin(2 * $latitude) + $CPrime * sin(4 * $latitude) - $DPrime * sin(6 * $latitude) + $EPrime * sin(8 * $latitude);
1250 9
        $m = $s - $sO;
1251 9
        $M = $kO * ($m + $A * $m ** 3);
1252 9
        $r = $rO - $M;
1253 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * sin($latitudeOrigin);
1254
1255 9
        $easting = $falseEasting->asMetres()->getValue() + $r * sin($theta);
1256 9
        $northing = $falseNorthing->asMetres()->getValue() + $M + $r * sin($theta) * tan($theta / 2);
1257
1258 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1259
    }
1260
1261
    /**
1262
     * Lambert Cylindrical Equal Area
1263
     * This is the ellipsoidal form of the projection.
1264
     */
1265 9
    public function lambertCylindricalEqualArea(
1266
        Projected $to,
1267
        Angle $latitudeOf1stStandardParallel,
1268
        Angle $longitudeOfNaturalOrigin,
1269
        Length $falseEasting,
1270
        Length $falseNorthing
1271
    ): ProjectedPoint {
1272 9
        $latitude = $this->latitude->asRadians()->getValue();
1273 9
        $latitudeFirstParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1274 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1275 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1276 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1277
1278 9
        $k = cos($latitudeFirstParallel) / sqrt(1 - $e2 * sin($latitudeFirstParallel) ** 2);
1279 9
        $q = (1 - $e2) * ((sin($latitude) / (1 - $e2 * sin($latitude) ** 2)) - (1 / (2 * $e)) * log((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))));
1280
1281 9
        $x = $a * $k * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1282 9
        $y = $a * $q / (2 * $k);
1283
1284 9
        $easting = $falseEasting->asMetres()->getValue() + $x;
1285 9
        $northing = $falseNorthing->asMetres()->getValue() + $y;
1286
1287 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1288
    }
1289
1290
    /**
1291
     * Modified Azimuthal Equidistant
1292
     * Modified form of Oblique Azimuthal Equidistant projection method developed for Polynesian islands. For the
1293
     * distances over which these projections are used (under 800km) this modification introduces no significant error.
1294
     */
1295 9
    public function modifiedAzimuthalEquidistant(
1296
        Projected $to,
1297
        Angle $latitudeOfNaturalOrigin,
1298
        Angle $longitudeOfNaturalOrigin,
1299
        Length $falseEasting,
1300
        Length $falseNorthing
1301
    ): ProjectedPoint {
1302 9
        $latitude = $this->latitude->asRadians()->getValue();
1303 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1304 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1305 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1306 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1307
1308 9
        $nuO = $a / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
1309 9
        $nu = $a / sqrt(1 - $e2 * sin($latitude) ** 2);
1310 9
        $psi = atan((1 - $e2) * tan($latitude) + ($e2 * $nuO * sin($latitudeOrigin)) / ($nu * cos($latitude)));
1311 9
        $alpha = atan2(sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()), (cos($latitudeOrigin) * tan($psi) - sin($latitudeOrigin) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
1312 9
        $G = $e * sin($latitudeOrigin) / sqrt(1 - $e2);
1313 9
        $H = $e * cos($latitudeOrigin) * cos($alpha) / sqrt(1 - $e2);
1314
1315 9
        if (sin($alpha) === 0.0) {
1316
            $s = self::asin(cos($latitudeOrigin) * sin($psi) - sin($latitudeOrigin) * cos($alpha)) * cos($alpha) / abs(cos($alpha));
1317
        } else {
1318 9
            $s = self::asin(sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()) * cos($psi) / sin($alpha));
1319
        }
1320
1321 9
        $c = $nuO * $s * ((1 - $s ** 2 * $H ** 2 * (1 - $H ** 2) / 6) + (($s ** 3 / 8) * $G * $H * (1 - 2 * $H ** 2)) + ($s ** 4 / 120) * ($H ** 2 * (4 - 7 * $H ** 2) - 3 * $G ** 2 * (1 - 7 * $H ** 2)) - (($s ** 5 / 48) * $G * $H));
1322
1323 9
        $easting = $falseEasting->asMetres()->getValue() + $c * sin($alpha);
1324 9
        $northing = $falseNorthing->asMetres()->getValue() + $c * cos($alpha);
1325
1326 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1327
    }
1328
1329
    /**
1330
     * Oblique Stereographic
1331
     * This is not the same as the projection method of the same name in USGS Professional Paper no. 1395, "Map
1332
     * Projections - A Working Manual" by John P. Snyder.
1333
     */
1334 9
    public function obliqueStereographic(
1335
        Projected $to,
1336
        Angle $latitudeOfNaturalOrigin,
1337
        Angle $longitudeOfNaturalOrigin,
1338
        Scale $scaleFactorAtNaturalOrigin,
1339
        Length $falseEasting,
1340
        Length $falseNorthing
1341
    ): ProjectedPoint {
1342 9
        $latitude = $this->latitude->asRadians()->getValue();
1343 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1344 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1345 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1346 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1347 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1348 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1349
1350 9
        $rhoOrigin = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** (3 / 2);
1351 9
        $nuOrigin = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
1352 9
        $R = sqrt($rhoOrigin * $nuOrigin);
1353
1354 9
        $n = sqrt(1 + ($e2 * cos($latitudeOrigin) ** 4 / (1 - $e2)));
1355 9
        $S1 = (1 + sin($latitudeOrigin)) / (1 - sin($latitudeOrigin));
1356 9
        $S2 = (1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin));
1357 9
        $w1 = ($S1 * ($S2 ** $e)) ** $n;
1358 9
        $c = (($n + sin($latitudeOrigin)) * (1 - ($w1 - 1) / ($w1 + 1))) / (($n - sin($latitudeOrigin)) * (1 + ($w1 - 1) / ($w1 + 1)));
1359 9
        $w2 = $c * $w1;
1360 9
        $chiOrigin = self::asin(($w2 - 1) / ($w2 + 1));
1361
1362 9
        $lambda = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() + $longitudeOrigin;
1363
1364 9
        $Sa = (1 + sin($latitude)) / (1 - sin($latitude));
1365 9
        $Sb = (1 - $e * sin($latitude)) / (1 + $e * sin($latitude));
1366 9
        $w = $c * ($Sa * ($Sb ** $e)) ** $n;
1367 9
        $chi = self::asin(($w - 1) / ($w + 1));
1368
1369 9
        $B = (1 + sin($chi) * sin($chiOrigin) + cos($chi) * cos($chiOrigin) * cos($lambda - $longitudeOrigin));
1370
1371 9
        $easting = $falseEasting->asMetres()->getValue() + 2 * $R * $kO * cos($chi) * sin($lambda - $longitudeOrigin) / $B;
1372 9
        $northing = $falseNorthing->asMetres()->getValue() + 2 * $R * $kO * (sin($chi) * cos($chiOrigin) - cos($chi) * sin($chiOrigin) * cos($lambda - $longitudeOrigin)) / $B;
1373
1374 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1375
    }
1376
1377
    /**
1378
     * Polar Stereographic (variant A)
1379
     * Latitude of natural origin must be either 90 degrees or -90 degrees (or equivalent in alternative angle unit).
1380
     */
1381 9
    public function polarStereographicVariantA(
1382
        Projected $to,
1383
        Angle $latitudeOfNaturalOrigin,
1384
        Angle $longitudeOfNaturalOrigin,
1385
        Scale $scaleFactorAtNaturalOrigin,
1386
        Length $falseEasting,
1387
        Length $falseNorthing
1388
    ): ProjectedPoint {
1389 9
        $latitude = $this->latitude->asRadians()->getValue();
1390 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1391 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1392 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1393 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1394
1395 9
        if ($latitudeOrigin < 0) {
1396
            $t = tan(M_PI / 4 + $latitude / 2) / (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1397
        } else {
1398 9
            $t = tan(M_PI / 4 - $latitude / 2) * (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1399
        }
1400 9
        $rho = 2 * $a * $kO * $t / sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e));
1401
1402 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1403 9
        $dE = $rho * sin($theta);
1404 9
        $dN = $rho * cos($theta);
1405
1406 9
        $easting = $falseEasting->asMetres()->getValue() + $dE;
1407 9
        if ($latitudeOrigin < 0) {
1408
            $northing = $falseNorthing->asMetres()->getValue() + $dN;
1409
        } else {
1410 9
            $northing = $falseNorthing->asMetres()->getValue() - $dN;
1411
        }
1412
1413 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1414
    }
1415
1416
    /**
1417
     * Polar Stereographic (variant B).
1418
     */
1419 9
    public function polarStereographicVariantB(
1420
        Projected $to,
1421
        Angle $latitudeOfStandardParallel,
1422
        Angle $longitudeOfOrigin,
1423
        Length $falseEasting,
1424
        Length $falseNorthing
1425
    ): ProjectedPoint {
1426 9
        $latitude = $this->latitude->asRadians()->getValue();
1427 9
        $firstStandardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1428 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1429 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1430 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1431
1432 9
        if ($firstStandardParallel < 0) {
1433 9
            $tF = tan(M_PI / 4 + $firstStandardParallel / 2) / (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1434 9
            $t = tan(M_PI / 4 + $latitude / 2) / (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1435
        } else {
1436
            $tF = tan(M_PI / 4 - $firstStandardParallel / 2) * (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1437
            $t = tan(M_PI / 4 - $latitude / 2) * (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1438
        }
1439 9
        $mF = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1440 9
        $kO = $mF * sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e)) / (2 * $tF);
1441
1442 9
        $rho = 2 * $a * $kO * $t / sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e));
1443
1444 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfOrigin))->asRadians()->getValue();
1445 9
        $dE = $rho * sin($theta);
1446 9
        $dN = $rho * cos($theta);
1447
1448 9
        $easting = $falseEasting->asMetres()->getValue() + $dE;
1449 9
        if ($firstStandardParallel < 0) {
1450 9
            $northing = $falseNorthing->asMetres()->getValue() + $dN;
1451
        } else {
1452
            $northing = $falseNorthing->asMetres()->getValue() - $dN;
1453
        }
1454
1455 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1456
    }
1457
1458
    /**
1459
     * Polar Stereographic (variant C).
1460
     */
1461 9
    public function polarStereographicVariantC(
1462
        Projected $to,
1463
        Angle $latitudeOfStandardParallel,
1464
        Angle $longitudeOfOrigin,
1465
        Length $eastingAtFalseOrigin,
1466
        Length $northingAtFalseOrigin
1467
    ): ProjectedPoint {
1468 9
        $latitude = $this->latitude->asRadians()->getValue();
1469 9
        $firstStandardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1470 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1471 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1472 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1473
1474 9
        if ($firstStandardParallel < 0) {
1475 9
            $tF = tan(M_PI / 4 + $firstStandardParallel / 2) / (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1476 9
            $t = tan(M_PI / 4 + $latitude / 2) / (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1477
        } else {
1478
            $tF = tan(M_PI / 4 - $firstStandardParallel / 2) * (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1479
            $t = tan(M_PI / 4 - $latitude / 2) * (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1480
        }
1481 9
        $mF = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1482
1483 9
        $rhoF = $a * $mF;
1484 9
        $rho = $rhoF * $t / $tF;
1485
1486 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfOrigin))->asRadians()->getValue();
1487 9
        $dE = $rho * sin($theta);
1488 9
        $dN = $rho * cos($theta);
1489
1490 9
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $dE;
1491 9
        if ($firstStandardParallel < 0) {
1492 9
            $northing = $northingAtFalseOrigin->asMetres()->getValue() - $rhoF + $dN;
1493
        } else {
1494
            $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rhoF - $dN;
1495
        }
1496
1497 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1498
    }
1499
1500
    /**
1501
     * Popular Visualisation Pseudo Mercator
1502
     * Applies spherical formulas to the ellipsoid. As such does not have the properties of a true Mercator projection.
1503
     */
1504 9
    public function popularVisualisationPseudoMercator(
1505
        Projected $to,
1506
        Angle $latitudeOfNaturalOrigin,
0 ignored issues
show
Unused Code introduced by
The parameter $latitudeOfNaturalOrigin is not used and could be removed. ( Ignorable by Annotation )

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

1506
        /** @scrutinizer ignore-unused */ Angle $latitudeOfNaturalOrigin,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1507
        Angle $longitudeOfNaturalOrigin,
1508
        Length $falseEasting,
1509
        Length $falseNorthing
1510
    ): ProjectedPoint {
1511 9
        $latitude = $this->latitude->asRadians()->getValue();
1512 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1513
1514 9
        $easting = $falseEasting->asMetres()->getValue() + $a * ($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue());
1515 9
        $northing = $falseNorthing->asMetres()->getValue() + $a * log(tan(M_PI / 4 + $latitude / 2));
1516
1517 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1518
    }
1519
1520
    /**
1521
     * Mercator (variant A)
1522
     * Note that in these formulas the parameter latitude of natural origin (latO) is not used. However for this
1523
     * Mercator (variant A) method the EPSG dataset includes this parameter, which must have a value of zero, for
1524
     * completeness in CRS labelling.
1525
     */
1526 18
    public function mercatorVariantA(
1527
        Projected $to,
1528
        Angle $latitudeOfNaturalOrigin,
0 ignored issues
show
Unused Code introduced by
The parameter $latitudeOfNaturalOrigin is not used and could be removed. ( Ignorable by Annotation )

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

1528
        /** @scrutinizer ignore-unused */ Angle $latitudeOfNaturalOrigin,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1529
        Angle $longitudeOfNaturalOrigin,
1530
        Scale $scaleFactorAtNaturalOrigin,
1531
        Length $falseEasting,
1532
        Length $falseNorthing
1533
    ): ProjectedPoint {
1534 18
        $latitude = $this->latitude->asRadians()->getValue();
1535 18
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1536
1537 18
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1538 18
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1539
1540 18
        $easting = $falseEasting->asMetres()->getValue() + $a * $kO * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1541 18
        $northing = $falseNorthing->asMetres()->getValue() + $a * $kO * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1542
1543 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1544
    }
1545
1546
    /**
1547
     * Mercator (variant B)
1548
     * Used for most nautical charts.
1549
     */
1550 9
    public function mercatorVariantB(
1551
        Projected $to,
1552
        Angle $latitudeOf1stStandardParallel,
1553
        Angle $longitudeOfNaturalOrigin,
1554
        Length $falseEasting,
1555
        Length $falseNorthing
1556
    ): ProjectedPoint {
1557 9
        $latitude = $this->latitude->asRadians()->getValue();
1558 9
        $firstStandardParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1559 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1560 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1561 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1562
1563 9
        $kO = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1564
1565 9
        $easting = $falseEasting->asMetres()->getValue() + $a * $kO * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1566 9
        $northing = $falseNorthing->asMetres()->getValue() + $a * $kO * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1567
1568 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1569
    }
1570
1571
    /**
1572
     * Longitude rotation
1573
     * This transformation allows calculation of the longitude of a point in the target system by adding the parameter
1574
     * value to the longitude value of the point in the source system.
1575
     */
1576 9
    public function longitudeRotation(
1577
        Geographic $to,
1578
        Angle $longitudeOffset
1579
    ): self {
1580 9
        $newLongitude = $this->longitude->add($longitudeOffset);
1581
1582 9
        return static::create($this->latitude, $newLongitude, $this->height, $to, $this->epoch);
1583
    }
1584
1585
    /**
1586
     * Hotine Oblique Mercator (variant A).
1587
     */
1588 9
    public function obliqueMercatorHotineVariantA(
1589
        Projected $to,
1590
        Angle $latitudeOfProjectionCentre,
1591
        Angle $longitudeOfProjectionCentre,
1592
        Angle $azimuthOfInitialLine,
1593
        Angle $angleFromRectifiedToSkewGrid,
1594
        Scale $scaleFactorOnInitialLine,
1595
        Length $falseEasting,
1596
        Length $falseNorthing
1597
    ): ProjectedPoint {
1598 9
        $latitude = $this->latitude->asRadians()->getValue();
1599 9
        $longitude = $this->longitude->asRadians()->getValue();
1600 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1601 9
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1602 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1603 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1604 9
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1605 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1606 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1607 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1608
1609 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1610 9
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1611 9
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1612 9
        $D = $B * sqrt((1 - $e2)) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1613 9
        $DD = max(1, $D ** 2);
1614 9
        $F = $D + sqrt($DD - 1) * static::sign($latC);
1615 9
        $H = $F * ($tO) ** $B;
1616 9
        $G = ($F - 1 / $F) / 2;
1617 9
        $gammaO = self::asin(sin($alphaC) / $D);
1618 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1619
1620 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1621 9
        $Q = $H / $t ** $B;
1622 9
        $S = ($Q - 1 / $Q) / 2;
1623 9
        $T = ($Q + 1 / $Q) / 2;
1624 9
        $V = sin($B * ($longitude - $lonO));
1625 9
        $U = (-$V * cos($gammaO) + $S * sin($gammaO)) / $T;
1626 9
        $v = $A * log((1 - $U) / (1 + $U)) / (2 * $B);
1627 9
        $u = $A * atan2(($S * cos($gammaO) + $V * sin($gammaO)), cos($B * ($longitude - $lonO))) / $B;
1628
1629 9
        $easting = $v * cos($gammaC) + $u * sin($gammaC) + $falseEasting->asMetres()->getValue();
1630 9
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $falseNorthing->asMetres()->getValue();
1631
1632 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1633
    }
1634
1635
    /**
1636
     * Hotine Oblique Mercator (variant B).
1637
     */
1638 9
    public function obliqueMercatorHotineVariantB(
1639
        Projected $to,
1640
        Angle $latitudeOfProjectionCentre,
1641
        Angle $longitudeOfProjectionCentre,
1642
        Angle $azimuthOfInitialLine,
1643
        Angle $angleFromRectifiedToSkewGrid,
1644
        Scale $scaleFactorOnInitialLine,
1645
        Length $eastingAtProjectionCentre,
1646
        Length $northingAtProjectionCentre
1647
    ): ProjectedPoint {
1648 9
        $latitude = $this->latitude->asRadians()->getValue();
1649 9
        $longitude = $this->longitude->asRadians()->getValue();
1650 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1651 9
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1652 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1653 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1654 9
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1655 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1656 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1657 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1658
1659 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1660 9
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1661 9
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1662 9
        $D = $B * sqrt((1 - $e2)) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1663 9
        $DD = max(1, $D ** 2);
1664 9
        $F = $D + sqrt($DD - 1) * static::sign($latC);
1665 9
        $H = $F * ($tO) ** $B;
1666 9
        $G = ($F - 1 / $F) / 2;
1667 9
        $gammaO = self::asin(sin($alphaC) / $D);
1668 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1669 9
        $vC = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $vC is dead and can be removed.
Loading history...
1670 9
        if ($alphaC === M_PI / 2) {
1671
            $uC = $A * ($lonC - $lonO);
1672
        } else {
1673 9
            $uC = ($A / $B) * atan2(sqrt($DD - 1), cos($alphaC)) * static::sign($latC);
1674
        }
1675
1676 9
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1677 9
        $Q = $H / $t ** $B;
1678 9
        $S = ($Q - 1 / $Q) / 2;
1679 9
        $T = ($Q + 1 / $Q) / 2;
1680 9
        $V = sin($B * ($longitude - $lonO));
1681 9
        $U = (-$V * cos($gammaO) + $S * sin($gammaO)) / $T;
1682 9
        $v = $A * log((1 - $U) / (1 + $U)) / (2 * $B);
1683
1684 9
        if ($alphaC === M_PI / 2) {
1685
            if ($longitude === $lonC) {
1686
                $u = 0;
1687
            } else {
1688
                $u = ($A * atan(($S * cos($gammaO) + $V * sin($gammaO)) / cos($B * ($longitude - $lonO))) / $B) - (abs($uC) * static::sign($latC) * static::sign($lonC - $longitude));
1689
            }
1690
        } else {
1691 9
            $u = ($A * atan2(($S * cos($gammaO) + $V * sin($gammaO)), cos($B * ($longitude - $lonO))) / $B) - (abs($uC) * static::sign($latC));
1692
        }
1693
1694 9
        $easting = $v * cos($gammaC) + $u * sin($gammaC) + $eastingAtProjectionCentre->asMetres()->getValue();
1695 9
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $northingAtProjectionCentre->asMetres()->getValue();
1696
1697 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1698
    }
1699
1700
    /**
1701
     * Laborde Oblique Mercator.
1702
     */
1703 9
    public function obliqueMercatorLaborde(
1704
        Projected $to,
1705
        Angle $latitudeOfProjectionCentre,
1706
        Angle $longitudeOfProjectionCentre,
1707
        Angle $azimuthOfInitialLine,
1708
        Scale $scaleFactorOnInitialLine,
1709
        Length $falseEasting,
1710
        Length $falseNorthing
1711
    ): ProjectedPoint {
1712 9
        $latitude = $this->latitude->asRadians()->getValue();
1713 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1714 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1715 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1716 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1717 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1718 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1719
1720 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1721 9
        $latS = self::asin(sin($latC) / $B);
1722 9
        $R = $a * $kC * (sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2));
1723 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));
1724
1725 9
        $L = $B * $this->normaliseLongitude($this->longitude->subtract($longitudeOfProjectionCentre))->asRadians()->getValue();
1726 9
        $q = $C + $B * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1727 9
        $P = 2 * atan(M_E ** $q) - M_PI / 2;
1728 9
        $U = cos($P) * cos($L) * cos($latS) + sin($P) * sin($latS);
1729 9
        $V = cos($P) * cos($L) * sin($latS) - sin($P) * cos($latS);
1730 9
        $W = cos($P) * sin($L);
1731 9
        $d = hypot($U, $V);
1732 9
        if ($d === 0.0) {
1733
            $LPrime = 0;
1734
            $PPrime = static::sign($W) * M_PI / 2;
1735
        } else {
1736 9
            $LPrime = 2 * atan($V / ($U + $d));
1737 9
            $PPrime = atan($W / $d);
1738
        }
1739 9
        $H = new ComplexNumber(-$LPrime, log(tan(M_PI / 4 + $PPrime / 2)));
1740 9
        $G = (new ComplexNumber(1 - cos(2 * $alphaC), sin(2 * $alphaC)))->divide(new ComplexNumber(12, 0));
1741
1742 9
        $easting = $falseEasting->asMetres()->getValue() + $R * $H->pow(3)->multiply($G)->add($H)->getImaginary();
1743 9
        $northing = $falseNorthing->asMetres()->getValue() + $R * $H->pow(3)->multiply($G)->add($H)->getReal();
1744
1745 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1746
    }
1747
1748
    /**
1749
     * Transverse Mercator.
1750
     */
1751 108
    public function transverseMercator(
1752
        Projected $to,
1753
        Angle $latitudeOfNaturalOrigin,
1754
        Angle $longitudeOfNaturalOrigin,
1755
        Scale $scaleFactorAtNaturalOrigin,
1756
        Length $falseEasting,
1757
        Length $falseNorthing
1758
    ): ProjectedPoint {
1759 108
        $latitude = $this->latitude->asRadians()->getValue();
1760 108
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1761 108
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1762 108
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1763 108
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1764 108
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
1765
1766 108
        $n = $f / (2 - $f);
1767 108
        $B = ($a / (1 + $n)) * (1 + $n ** 2 / 4 + $n ** 4 / 64);
1768
1769 108
        $h1 = $n / 2 - (2 / 3) * $n ** 2 + (5 / 16) * $n ** 3 + (41 / 180) * $n ** 4;
1770 108
        $h2 = (13 / 48) * $n ** 2 - (3 / 5) * $n ** 3 + (557 / 1440) * $n ** 4;
1771 108
        $h3 = (61 / 240) * $n ** 3 - (103 / 140) * $n ** 4;
1772 108
        $h4 = (49561 / 161280) * $n ** 4;
1773
1774 108
        if ($latitudeOrigin === 0.0) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === 0.0 is always false.
Loading history...
1775 81
            $mO = 0;
1776 27
        } elseif ($latitudeOrigin === M_PI / 2) {
1777
            $mO = $B * M_PI / 2;
1778 27
        } elseif ($latitudeOrigin === -M_PI / 2) {
1779
            $mO = $B * -M_PI / 2;
1780
        } else {
1781 27
            $qO = asinh(tan($latitudeOrigin)) - ($e * atanh($e * sin($latitudeOrigin)));
1782 27
            $betaO = atan(sinh($qO));
1783 27
            $xiO0 = self::asin(sin($betaO));
1784 27
            $xiO1 = $h1 * sin(2 * $xiO0);
1785 27
            $xiO2 = $h2 * sin(4 * $xiO0);
1786 27
            $xiO3 = $h3 * sin(6 * $xiO0);
1787 27
            $xiO4 = $h4 * sin(8 * $xiO0);
1788 27
            $xiO = $xiO0 + $xiO1 + $xiO2 + $xiO3 + $xiO4;
1789 27
            $mO = $B * $xiO;
1790
        }
1791
1792 108
        $Q = asinh(tan($latitude)) - ($e * atanh($e * sin($latitude)));
1793 108
        $beta = atan(sinh($Q));
1794 108
        $eta0 = atanh(cos($beta) * sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()));
1795 108
        $xi0 = self::asin(sin($beta) * cosh($eta0));
1796 108
        $xi1 = $h1 * sin(2 * $xi0) * cosh(2 * $eta0);
1797 108
        $eta1 = $h1 * cos(2 * $xi0) * sinh(2 * $eta0);
1798 108
        $xi2 = $h2 * sin(4 * $xi0) * cosh(4 * $eta0);
1799 108
        $eta2 = $h2 * cos(4 * $xi0) * sinh(4 * $eta0);
1800 108
        $xi3 = $h3 * sin(6 * $xi0) * cosh(6 * $eta0);
1801 108
        $eta3 = $h3 * cos(6 * $xi0) * sinh(6 * $eta0);
1802 108
        $xi4 = $h4 * sin(8 * $xi0) * cosh(8 * $eta0);
1803 108
        $eta4 = $h4 * cos(8 * $xi0) * sinh(8 * $eta0);
1804 108
        $xi = $xi0 + $xi1 + $xi2 + $xi3 + $xi4;
1805 108
        $eta = $eta0 + $eta1 + $eta2 + $eta3 + $eta4;
1806
1807 108
        $easting = $falseEasting->asMetres()->getValue() + $kO * $B * $eta;
1808 108
        $northing = $falseNorthing->asMetres()->getValue() + $kO * ($B * $xi - $mO);
1809
1810 108
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1811
    }
1812
1813
    /**
1814
     * Transverse Mercator Zoned Grid System
1815
     * If locations fall outwith the fixed zones the general Transverse Mercator method (code 9807) must be used for
1816
     * each zone.
1817
     */
1818 36
    public function transverseMercatorZonedGrid(
1819
        Projected $to,
1820
        Angle $latitudeOfNaturalOrigin,
1821
        Angle $initialLongitude,
1822
        Angle $zoneWidth,
1823
        Scale $scaleFactorAtNaturalOrigin,
1824
        Length $falseEasting,
1825
        Length $falseNorthing
1826
    ): ProjectedPoint {
1827 36
        $W = $zoneWidth->asDegrees()->getValue();
1828 36
        $Z = ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / $W) % (360 / $W) + 1;
1829
1830 36
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * $W - $W / 2));
1831 36
        $falseEasting = $falseEasting->add(new Metre($Z * 1000000));
1832
1833 36
        return $this->transverseMercator($to, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
1834
    }
1835
1836
    /**
1837
     * New Zealand Map Grid.
1838
     */
1839 27
    public function newZealandMapGrid(
1840
        Projected $to,
1841
        Angle $latitudeOfNaturalOrigin,
1842
        Angle $longitudeOfNaturalOrigin,
1843
        Length $falseEasting,
1844
        Length $falseNorthing
1845
    ): ProjectedPoint {
1846 27
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1847
1848 27
        $deltaLatitudeToOrigin = Angle::convert($this->latitude->subtract($latitudeOfNaturalOrigin), Angle::EPSG_ARC_SECOND)->getValue();
1849 27
        $deltaLongitudeToOrigin = $this->longitude->subtract($longitudeOfNaturalOrigin)->asRadians();
1850
1851 27
        $deltaPsi = 0;
1852 27
        $deltaPsi += 0.6399175073 * ($deltaLatitudeToOrigin * 0.00001) ** 1;
1853 27
        $deltaPsi += -0.1358797613 * ($deltaLatitudeToOrigin * 0.00001) ** 2;
1854 27
        $deltaPsi += 0.063294409 * ($deltaLatitudeToOrigin * 0.00001) ** 3;
1855 27
        $deltaPsi += -0.02526853 * ($deltaLatitudeToOrigin * 0.00001) ** 4;
1856 27
        $deltaPsi += 0.0117879 * ($deltaLatitudeToOrigin * 0.00001) ** 5;
1857 27
        $deltaPsi += -0.0055161 * ($deltaLatitudeToOrigin * 0.00001) ** 6;
1858 27
        $deltaPsi += 0.0026906 * ($deltaLatitudeToOrigin * 0.00001) ** 7;
1859 27
        $deltaPsi += -0.001333 * ($deltaLatitudeToOrigin * 0.00001) ** 8;
1860 27
        $deltaPsi += 0.00067 * ($deltaLatitudeToOrigin * 0.00001) ** 9;
1861 27
        $deltaPsi += -0.00034 * ($deltaLatitudeToOrigin * 0.00001) ** 10;
1862
1863 27
        $zeta = new ComplexNumber($deltaPsi, $deltaLongitudeToOrigin->getValue());
1864
1865 27
        $B1 = new ComplexNumber(0.7557853228, 0.0);
1866 27
        $B2 = new ComplexNumber(0.249204646, 0.003371507);
1867 27
        $B3 = new ComplexNumber(-0.001541739, 0.041058560);
1868 27
        $B4 = new ComplexNumber(-0.10162907, 0.01727609);
1869 27
        $B5 = new ComplexNumber(-0.26623489, -0.36249218);
1870 27
        $B6 = new ComplexNumber(-0.6870983, -1.1651967);
1871 27
        $z = new ComplexNumber(0, 0);
1872 27
        $z = $z->add($B1->multiply($zeta->pow(1)));
1873 27
        $z = $z->add($B2->multiply($zeta->pow(2)));
1874 27
        $z = $z->add($B3->multiply($zeta->pow(3)));
1875 27
        $z = $z->add($B4->multiply($zeta->pow(4)));
1876 27
        $z = $z->add($B5->multiply($zeta->pow(5)));
1877 27
        $z = $z->add($B6->multiply($zeta->pow(6)));
1878
1879 27
        $easting = $falseEasting->asMetres()->getValue() + $z->getImaginary() * $a;
1880 27
        $northing = $falseNorthing->asMetres()->getValue() + $z->getReal() * $a;
1881
1882 27
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1883
    }
1884
1885
    /**
1886
     * Madrid to ED50 polynomial.
1887
     */
1888 9
    public function madridToED50Polynomial(
1889
        Geographic2D $to,
1890
        Scale $A0,
1891
        Scale $A1,
1892
        Scale $A2,
1893
        Scale $A3,
1894
        Angle $B00,
1895
        Scale $B0,
1896
        Scale $B1,
1897
        Scale $B2,
1898
        Scale $B3
1899
    ): self {
1900 9
        $dLatitude = new ArcSecond($A0->add($A1->multiply($this->latitude->getValue()))->add($A2->multiply($this->longitude->getValue()))->add($A3->multiply($this->height ? $this->height->getValue() : 0))->getValue());
1901 9
        $dLongitude = $B00->add(new ArcSecond($B0->add($B1->multiply($this->latitude->getValue()))->add($B2->multiply($this->longitude->getValue()))->add($B3->multiply($this->height ? $this->height->getValue() : 0))->getValue()));
1902
1903 9
        return self::create($this->latitude->add($dLatitude), $this->longitude->add($dLongitude), null, $to, $this->epoch);
1904
    }
1905
1906
    /**
1907
     * Geographic3D to 2D conversion.
1908
     */
1909 45
    public function threeDToTwoD(
1910
        Geographic $to
1911
    ): self {
1912 45
        if ($to instanceof Geographic2D) {
1913 45
            return static::create($this->latitude, $this->longitude, null, $to, $this->epoch);
1914
        }
1915
1916
        return static::create($this->latitude, $this->longitude, new Metre(0), $to, $this->epoch);
1917
    }
1918
1919
    /**
1920
     * Geographic2D offsets.
1921
     * This transformation allows calculation of coordinates in the target system by adding the parameter value to the
1922
     * coordinate values of the point in the source system.
1923
     */
1924 9
    public function geographic2DOffsets(
1925
        Geographic $to,
1926
        Angle $latitudeOffset,
1927
        Angle $longitudeOffset
1928
    ): self {
1929 9
        $toLatitude = $this->latitude->add($latitudeOffset);
1930 9
        $toLongitude = $this->longitude->add($longitudeOffset);
1931
1932 9
        return static::create($toLatitude, $toLongitude, null, $to, $this->epoch);
1933
    }
1934
1935
    /*
1936
     * Geographic2D with Height Offsets.
1937
     * This transformation allows calculation of coordinates in the target system by adding the parameter value to the
1938
     * coordinate values of the point in the source system.
1939
     */
1940
    public function geographic2DWithHeightOffsets(
1941
        Compound $to,
1942
        Angle $latitudeOffset,
1943
        Angle $longitudeOffset,
1944
        Length $geoidUndulation
1945
    ): CompoundPoint {
1946
        $toLatitude = $this->latitude->add($latitudeOffset);
1947
        $toLongitude = $this->longitude->add($longitudeOffset);
1948
        $toHeight = $this->height->add($geoidUndulation);
0 ignored issues
show
Bug introduced by
The method add() does not exist on null. ( Ignorable by Annotation )

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

1948
        /** @scrutinizer ignore-call */ 
1949
        $toHeight = $this->height->add($geoidUndulation);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1949
1950
        $horizontal = static::create($toLatitude, $toLongitude, null, $to->getHorizontal(), $this->epoch);
1951
        $vertical = VerticalPoint::create($toHeight, $to->getVertical(), $this->epoch);
0 ignored issues
show
Bug introduced by
It seems like $toHeight can also be of type null; however, parameter $height of PHPCoord\VerticalPoint::create() 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

1951
        $vertical = VerticalPoint::create(/** @scrutinizer ignore-type */ $toHeight, $to->getVertical(), $this->epoch);
Loading history...
1952
1953
        return CompoundPoint::create($horizontal, $vertical, $to, $this->epoch);
1954
    }
1955
1956
    /**
1957
     * General polynomial.
1958
     * @param Coefficient[] $powerCoefficients
1959
     */
1960 18
    public function generalPolynomial(
1961
        Geographic $to,
1962
        Angle $ordinate1OfEvaluationPointInSourceCRS,
1963
        Angle $ordinate2OfEvaluationPointInSourceCRS,
1964
        Angle $ordinate1OfEvaluationPointInTargetCRS,
1965
        Angle $ordinate2OfEvaluationPointInTargetCRS,
1966
        Scale $scalingFactorForSourceCRSCoordDifferences,
1967
        Scale $scalingFactorForTargetCRSCoordDifferences,
1968
        Scale $A0,
1969
        Scale $B0,
1970
        array $powerCoefficients
1971
    ): self {
1972 18
        $xs = $this->latitude->getValue();
1973 18
        $ys = $this->longitude->getValue();
1974
1975 18
        $t = $this->generalPolynomialUnitless(
1976 18
            $xs,
1977
            $ys,
1978
            $ordinate1OfEvaluationPointInSourceCRS,
1979
            $ordinate2OfEvaluationPointInSourceCRS,
1980
            $ordinate1OfEvaluationPointInTargetCRS,
1981
            $ordinate2OfEvaluationPointInTargetCRS,
1982
            $scalingFactorForSourceCRSCoordDifferences,
1983
            $scalingFactorForTargetCRSCoordDifferences,
1984
            $A0,
1985
            $B0,
1986
            $powerCoefficients
1987
        );
1988
1989 18
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
1990 18
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1991 18
            $xtUnit = Angle::EPSG_DEGREE;
1992
        }
1993 18
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
1994 18
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1995 18
            $ytUnit = Angle::EPSG_DEGREE;
1996
        }
1997
1998 18
        return static::create(
1999 18
            Angle::makeUnit($t['xt'], $xtUnit),
2000 18
            Angle::makeUnit($t['yt'], $ytUnit),
2001 18
            $this->height,
2002
            $to,
2003 18
            $this->epoch
2004
        );
2005
    }
2006
2007
    /**
2008
     * Reversible polynomial.
2009
     * @param Coefficient[] $powerCoefficients
2010
     */
2011 36
    public function reversiblePolynomial(
2012
        Geographic $to,
2013
        Angle $ordinate1OfEvaluationPoint,
2014
        Angle $ordinate2OfEvaluationPoint,
2015
        Scale $scalingFactorForCoordDifferences,
2016
        Scale $A0,
2017
        Scale $B0,
2018
        $powerCoefficients
2019
    ): self {
2020 36
        $xs = $this->latitude->getValue();
2021 36
        $ys = $this->longitude->getValue();
2022
2023 36
        $t = $this->reversiblePolynomialUnitless(
2024 36
            $xs,
2025
            $ys,
2026
            $ordinate1OfEvaluationPoint,
2027
            $ordinate2OfEvaluationPoint,
2028
            $scalingFactorForCoordDifferences,
2029
            $A0,
2030
            $B0,
2031
            $powerCoefficients
2032
        );
2033
2034 36
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2035 36
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2036 36
            $xtUnit = Angle::EPSG_DEGREE;
2037
        }
2038 36
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2039 36
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2040 36
            $ytUnit = Angle::EPSG_DEGREE;
2041
        }
2042
2043 36
        return static::create(
2044 36
            Angle::makeUnit($t['xt'], $xtUnit),
2045 36
            Angle::makeUnit($t['yt'], $ytUnit),
2046 36
            $this->height,
2047
            $to,
2048 36
            $this->epoch
2049
        );
2050
    }
2051
2052
    /**
2053
     * Axis Order Reversal.
2054
     */
2055
    public function axisReversal(
2056
        Geographic $to
2057
    ): self {
2058
        // axes are read in from the CRS, this is a book-keeping adjustment only
2059
        return static::create($this->latitude, $this->longitude, $this->height, $to, $this->epoch);
2060
    }
2061
2062
    /**
2063
     * Ordnance Survey National Transformation
2064
     * Geodetic transformation between ETRS89 (or WGS 84) and OSGB36 / National Grid.  Uses ETRS89 / National Grid as
2065
     * an intermediate coordinate system for bi-linear interpolation of gridded grid coordinate differences.
2066
     */
2067
    public function OSTN15(
2068
        Projected $to,
2069
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2070
    ): ProjectedPoint {
2071
        $etrs89NationalGrid = new Projected(
2072
            'ETRS89 / National Grid',
2073
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2074
            Datum::fromSRID(Datum::EPSG_EUROPEAN_TERRESTRIAL_REFERENCE_SYSTEM_1989_ENSEMBLE),
2075
            $to->getBoundingArea()
2076
        );
2077
2078
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2079
2080
        return $eastingAndNorthingDifferenceFile->applyForwardAdjustment($projected);
2081
    }
2082
2083
    /**
2084
     * NADCON5.
2085
     * Geodetic transformation operating on geographic coordinate differences by bi-quadratic interpolation.  Input
2086
     * expects longitudes to be positive east in range 0-360° (0° = Greenwich).
2087
     */
2088
    public function NADCON5(
2089
        Geographic $to,
2090
        NADCON5Grid $latitudeDifferenceFile,
2091
        NADCON5Grid $longitudeDifferenceFile,
2092
        ?NADCON5Grid $ellipsoidalHeightDifferenceFile = null,
2093
        bool $inReverse
2094
    ): self {
2095
        /*
2096
         * Ideally most of this logic (especially reverse case) would be in the NADCON5Grid class like the other grids,
2097
         * but NADCON5 uses different files for latitude/longitude/height that need to be combined at runtime so that
2098
         * isn't possible.
2099
         */
2100
        if (!$inReverse) {
2101
            $latitudeAdjustment = new ArcSecond($latitudeDifferenceFile->getForwardAdjustment($this));
2102
            $longitudeAdjustment = new ArcSecond($longitudeDifferenceFile->getForwardAdjustment($this));
2103
            $heightAdjustment = $this->getHeight() && $ellipsoidalHeightDifferenceFile ? new Metre($ellipsoidalHeightDifferenceFile->getForwardAdjustment($this)) : null;
2104
2105
            return self::create($this->latitude->add($latitudeAdjustment), $this->longitude->add($longitudeAdjustment), $heightAdjustment ? $this->height->add($heightAdjustment) : null, $to, $this->getCoordinateEpoch());
2106
        }
2107
2108
        $iteration = $this;
2109
2110
        do {
2111
            $prevIteration = $iteration;
2112
            $latitudeAdjustment = new ArcSecond($latitudeDifferenceFile->getForwardAdjustment($iteration));
2113
            $longitudeAdjustment = new ArcSecond($longitudeDifferenceFile->getForwardAdjustment($iteration));
2114
            $heightAdjustment = $this->getHeight() && $ellipsoidalHeightDifferenceFile ? new Metre($ellipsoidalHeightDifferenceFile->getForwardAdjustment($iteration)) : null;
2115
            $iteration = self::create($this->latitude->subtract($latitudeAdjustment), $this->longitude->subtract($longitudeAdjustment), $heightAdjustment ? $this->height->subtract($heightAdjustment) : null, $to, $this->getCoordinateEpoch());
2116
        } while (abs($iteration->latitude->subtract($prevIteration->latitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && abs($iteration->longitude->subtract($prevIteration->longitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && ($this->height === null || abs($iteration->height->subtract($prevIteration->height)->getValue()) > self::ITERATION_CONVERGENCE_GRID));
0 ignored issues
show
Bug introduced by
It seems like $prevIteration->height can also be of type null; however, parameter $unit of PHPCoord\UnitOfMeasure\Length\Length::subtract() 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

2116
        } while (abs($iteration->latitude->subtract($prevIteration->latitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && abs($iteration->longitude->subtract($prevIteration->longitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && ($this->height === null || abs($iteration->height->subtract(/** @scrutinizer ignore-type */ $prevIteration->height)->getValue()) > self::ITERATION_CONVERGENCE_GRID));
Loading history...
Bug introduced by
The method subtract() does not exist on null. ( Ignorable by Annotation )

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

2116
        } while (abs($iteration->latitude->subtract($prevIteration->latitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && abs($iteration->longitude->subtract($prevIteration->longitude)->getValue()) > self::ITERATION_CONVERGENCE_GRID && ($this->height === null || abs($iteration->height->/** @scrutinizer ignore-call */ subtract($prevIteration->height)->getValue()) > self::ITERATION_CONVERGENCE_GRID));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2117
2118
        return $iteration;
2119
    }
2120
2121
    /**
2122
     * NTv2
2123
     * Geodetic transformation operating on geographic coordinate differences by bi-linear interpolation.  Supersedes
2124
     * NTv1 (transformation method code 9614).  Input expects longitudes to be positive west.
2125
     */
2126
    public function NTv2(
2127
        Geographic $to,
2128
        NTv2Grid $latitudeAndLongitudeDifferenceFile,
2129
        bool $inReverse
2130
    ): self {
2131
        if (!$inReverse) {
2132
            return $latitudeAndLongitudeDifferenceFile->applyForwardAdjustment($this, $to);
2133
        } else {
2134
            return $latitudeAndLongitudeDifferenceFile->applyReverseAdjustment($this, $to);
2135
        }
2136
    }
2137
2138 351
    public function asGeographicValue(): GeographicValue
2139
    {
2140 351
        return new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
2141
    }
2142
2143 18
    public function asUTMPoint(): UTMPoint
2144
    {
2145 18
        $hemisphere = $this->getLatitude()->asDegrees()->getValue() >= 0 ? UTMPoint::HEMISPHERE_NORTH : UTMPoint::HEMISPHERE_SOUTH;
2146 18
        $latitudeOfNaturalOrigin = new Degree(0);
2147 18
        $initialLongitude = new Degree(-180);
2148 18
        $scaleFactorAtNaturalOrigin = new Unity(0.9996);
2149 18
        $falseEasting = new Metre(500000);
2150 18
        $falseNorthing = $hemisphere === UTMPoint::HEMISPHERE_NORTH ? new Metre(0) : new Metre(10000000);
2151 18
        $Z = ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / 6) % (360 / 6) + 1;
2152 18
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * 6 - 3));
2153
2154 18
        $projectedCRS = new Projected(
2155 18
            'UTM/' . $this->crs->getSRID(),
2156 18
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2157 18
            $this->crs->getDatum(),
2158 18
            BoundingArea::createWorld() // this is a dummy CRS for the transform only, details don't matter
2159
        );
2160
2161 18
        $asProjected = $this->transverseMercator($projectedCRS, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
2162
2163 18
        return new UTMPoint($asProjected->getEasting(), $asProjected->getNorthing(), $Z, $hemisphere, $this->crs, $this->epoch);
2164
    }
2165
}
2166