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

GeographicPoint::coordinateFrameRotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 12
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 22
ccs 5
cts 5
cp 1
crap 1
rs 9.8666

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

1504
        /** @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...
1505
        Angle $longitudeOfNaturalOrigin,
1506 9
        Length $falseEasting,
1507 9
        Length $falseNorthing
1508
    ): ProjectedPoint {
1509 9
        $latitude = $this->latitude->asRadians()->getValue();
1510 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1511
1512 9
        $easting = $falseEasting->asMetres()->getValue() + $a * ($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue());
1513
        $northing = $falseNorthing->asMetres()->getValue() + $a * log(tan(M_PI / 4 + $latitude / 2));
1514
1515
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1516
    }
1517
1518
    /**
1519
     * Mercator (variant A)
1520
     * Note that in these formulas the parameter latitude of natural origin (latO) is not used. However for this
1521 18
     * Mercator (variant A) method the EPSG dataset includes this parameter, which must have a value of zero, for
1522
     * completeness in CRS labelling.
1523
     */
1524
    public function mercatorVariantA(
1525
        Projected $to,
1526
        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

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

1946
        /** @scrutinizer ignore-call */ 
1947
        $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...
1947
1948
        $horizontal = static::create($toLatitude, $toLongitude, null, $to->getHorizontal(), $this->epoch);
1949
        $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

1949
        $vertical = VerticalPoint::create(/** @scrutinizer ignore-type */ $toHeight, $to->getVertical(), $this->epoch);
Loading history...
1950
1951
        return CompoundPoint::create($horizontal, $vertical, $to, $this->epoch);
1952
    }
1953
1954
    /**
1955 18
     * General polynomial.
1956
     * @param Coefficient[] $powerCoefficients
1957
     */
1958
    public function generalPolynomial(
1959
        Geographic $to,
1960
        Angle $ordinate1OfEvaluationPointInSourceCRS,
1961
        Angle $ordinate2OfEvaluationPointInSourceCRS,
1962
        Angle $ordinate1OfEvaluationPointInTargetCRS,
1963
        Angle $ordinate2OfEvaluationPointInTargetCRS,
1964
        Scale $scalingFactorForSourceCRSCoordDifferences,
1965
        Scale $scalingFactorForTargetCRSCoordDifferences,
1966
        Scale $A0,
1967 18
        Scale $B0,
1968 18
        array $powerCoefficients
1969
    ): self {
1970 18
        $xs = $this->latitude->getValue();
1971 18
        $ys = $this->longitude->getValue();
1972
1973
        $t = $this->generalPolynomialUnitless(
1974
            $xs,
1975
            $ys,
1976
            $ordinate1OfEvaluationPointInSourceCRS,
1977
            $ordinate2OfEvaluationPointInSourceCRS,
1978
            $ordinate1OfEvaluationPointInTargetCRS,
1979
            $ordinate2OfEvaluationPointInTargetCRS,
1980
            $scalingFactorForSourceCRSCoordDifferences,
1981
            $scalingFactorForTargetCRSCoordDifferences,
1982
            $A0,
1983
            $B0,
1984 18
            $powerCoefficients
1985 18
        );
1986 18
1987
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
1988 18
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1989 18
            $xtUnit = Angle::EPSG_DEGREE;
1990 18
        }
1991
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
1992
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1993 18
            $ytUnit = Angle::EPSG_DEGREE;
1994 18
        }
1995 18
1996 18
        return static::create(
1997
            Angle::makeUnit($t['xt'], $xtUnit),
1998 18
            Angle::makeUnit($t['yt'], $ytUnit),
1999
            $this->height,
2000
            $to,
2001
            $this->epoch
2002
        );
2003
    }
2004
2005
    /**
2006 36
     * Reversible polynomial.
2007
     * @param Coefficient[] $powerCoefficients
2008
     */
2009
    public function reversiblePolynomial(
2010
        Geographic $to,
2011
        Angle $ordinate1OfEvaluationPoint,
2012
        Angle $ordinate2OfEvaluationPoint,
2013
        Scale $scalingFactorForCoordDifferences,
2014
        Scale $A0,
2015 36
        Scale $B0,
2016 36
        $powerCoefficients
2017
    ): self {
2018 36
        $xs = $this->latitude->getValue();
2019 36
        $ys = $this->longitude->getValue();
2020
2021
        $t = $this->reversiblePolynomialUnitless(
2022
            $xs,
2023
            $ys,
2024
            $ordinate1OfEvaluationPoint,
2025
            $ordinate2OfEvaluationPoint,
2026
            $scalingFactorForCoordDifferences,
2027
            $A0,
2028
            $B0,
2029 36
            $powerCoefficients
2030 36
        );
2031 36
2032
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2033 36
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2034 36
            $xtUnit = Angle::EPSG_DEGREE;
2035 36
        }
2036
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2037
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2038 36
            $ytUnit = Angle::EPSG_DEGREE;
2039 36
        }
2040 36
2041 36
        return static::create(
2042
            Angle::makeUnit($t['xt'], $xtUnit),
2043 36
            Angle::makeUnit($t['yt'], $ytUnit),
2044
            $this->height,
2045
            $to,
2046
            $this->epoch
2047
        );
2048
    }
2049
2050
    /**
2051
     * Axis Order Reversal.
2052
     */
2053
    public function axisReversal(
2054
        Geographic $to
2055
    ): self {
2056
        // axes are read in from the CRS, this is a book-keeping adjustment only
2057 333
        return static::create($this->latitude, $this->longitude, $this->height, $to, $this->epoch);
2058
    }
2059 333
2060
    /**
2061
     * Ordnance Survey National Transformation
2062 18
     * Geodetic transformation between ETRS89 (or WGS 84) and OSGB36 / National Grid.  Uses ETRS89 / National Grid as
2063
     * an intermediate coordinate system for bi-linear interpolation of gridded grid coordinate differences.
2064 18
     */
2065 18
    public function OSTN15(
2066 18
        Projected $to,
2067 18
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2068 18
    ): ProjectedPoint {
2069 18
        $etrs89NationalGrid = new Projected(
2070 18
            'ETRS89 / National Grid',
2071 18
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2072
            Datum::fromSRID(Datum::EPSG_EUROPEAN_TERRESTRIAL_REFERENCE_SYSTEM_1989_ENSEMBLE),
2073 18
            $to->getBoundingArea()
2074 18
        );
2075 18
2076 18
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2077 18
2078
        return $eastingAndNorthingDifferenceFile->applyForwardAdjustment($projected);
2079
    }
2080 18
2081
    public function asGeographicValue(): GeographicValue
2082 18
    {
2083
        return new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
2084
    }
2085
2086
    public function asUTMPoint(): UTMPoint
2087
    {
2088
        $hemisphere = $this->getLatitude()->asDegrees()->getValue() >= 0 ? UTMPoint::HEMISPHERE_NORTH : UTMPoint::HEMISPHERE_SOUTH;
2089
        $latitudeOfNaturalOrigin = new Degree(0);
2090
        $initialLongitude = new Degree(-180);
2091
        $scaleFactorAtNaturalOrigin = new Unity(0.9996);
2092
        $falseEasting = new Metre(500000);
2093
        $falseNorthing = $hemisphere === UTMPoint::HEMISPHERE_NORTH ? new Metre(0) : new Metre(10000000);
2094
        $Z = ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / 6) % (360 / 6) + 1;
2095
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * 6 - 3));
2096
2097
        $projectedCRS = new Projected(
2098
            'UTM/' . $this->crs->getSRID(),
2099
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2100
            $this->crs->getDatum(),
2101
            BoundingArea::createWorld() // this is a dummy CRS for the transform only, details don't matter
2102
        );
2103
2104
        $asProjected = $this->transverseMercator($projectedCRS, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
2105
2106
        return new UTMPoint($asProjected->getEasting(), $asProjected->getNorthing(), $Z, $hemisphere, $this->crs, $this->epoch);
2107
    }
2108
}
2109