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

coordinateFrameMolodenskyBadekas()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 21
c 0
b 0
f 0
dl 0
loc 37
ccs 22
cts 22
cp 1
rs 9.584
cc 2
nc 1
nop 11
crap 2

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
     */
94
    protected ?DateTimeImmutable $epoch;
95
96 1449
    protected function __construct(Angle $latitude, Angle $longitude, ?Length $height, Geographic $crs, ?DateTimeInterface $epoch = null)
97
    {
98 1449
        if (!$crs instanceof Geographic2D && !$crs instanceof Geographic3D) {
99
            throw new TypeError(sprintf("A geographic point must be associated with a geographic CRS, but a '%s' CRS was given", get_class($crs)));
100
        }
101
102 1449
        if ($crs instanceof Geographic2D && $height !== null) {
103 9
            throw new InvalidCoordinateReferenceSystemException('A 2D geographic point must not include a height');
104
        }
105
106 1440
        if ($crs instanceof Geographic3D && $height === null) {
107 9
            throw new InvalidCoordinateReferenceSystemException('A 3D geographic point must include a height, none given');
108
        }
109
110 1431
        $this->crs = $crs;
111
112 1431
        $latitude = $this->normaliseLatitude($latitude);
113 1431
        $longitude = $this->normaliseLongitude($longitude);
114
115 1431
        $this->latitude = Angle::convert($latitude, $this->getAxisByName(Axis::GEODETIC_LATITUDE)->getUnitOfMeasureId());
116 1431
        $this->longitude = Angle::convert($longitude, $this->getAxisByName(Axis::GEODETIC_LONGITUDE)->getUnitOfMeasureId());
117
118 1431
        if ($height) {
119 180
            $this->height = Length::convert($height, $this->getAxisByName(Axis::ELLIPSOIDAL_HEIGHT)->getUnitOfMeasureId());
120
        } else {
121 1323
            $this->height = null;
122
        }
123
124 1431
        if ($epoch instanceof DateTime) {
125 9
            $epoch = DateTimeImmutable::createFromMutable($epoch);
126
        }
127 1431
        $this->epoch = $epoch;
128 1431
    }
129
130
    /**
131
     * @param Angle   $latitude  refer to CRS for preferred unit of measure, but any angle unit accepted
132
     * @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
     */
135 1449
    public static function create(Angle $latitude, Angle $longitude, ?Length $height = null, Geographic $crs, ?DateTimeInterface $epoch = null): self
136
    {
137 1449
        return new static($latitude, $longitude, $height, $crs, $epoch);
138
    }
139
140 738
    public function getLatitude(): Angle
141
    {
142 738
        return $this->latitude;
143
    }
144
145 720
    public function getLongitude(): Angle
146
    {
147 720
        return $this->longitude;
148
    }
149
150 333
    public function getHeight(): ?Length
151
    {
152 333
        return $this->height;
153
    }
154
155 1431
    public function getCRS(): Geographic
156
    {
157 1431
        return $this->crs;
158
    }
159
160 162
    public function getCoordinateEpoch(): ?DateTimeImmutable
161
    {
162 162
        return $this->epoch;
163
    }
164
165 1431
    protected function normaliseLatitude(Angle $latitude): Angle
166
    {
167 1431
        if ($latitude->asDegrees()->getValue() > 90) {
168
            return new Degree(90);
169
        }
170 1431
        if ($latitude->asDegrees()->getValue() < -90) {
171
            return new Degree(-90);
172
        }
173
174 1431
        return $latitude;
175
    }
176
177 1431
    protected function normaliseLongitude(Angle $longitude): Angle
178
    {
179 1431
        while ($longitude->asDegrees()->getValue() > 180) {
180 9
            $longitude = $longitude->subtract(new Degree(360));
181
        }
182 1431
        while ($longitude->asDegrees()->getValue() <= -180) {
183
            $longitude = $longitude->add(new Degree(360));
184
        }
185
186 1431
        return $longitude;
187
    }
188
189
    /**
190
     * Calculate surface distance between two points.
191
     */
192 153
    public function calculateDistance(Point $to): Length
193
    {
194
        try {
195 153
            if ($to instanceof ConvertiblePoint) {
196 144
                $to = $to->convert($this->crs);
197
            }
198
        } finally {
199 153
            if ($to->getCRS()->getSRID() !== $this->crs->getSRID()) {
200 9
                throw new InvalidCoordinateReferenceSystemException('Can only calculate distances between two points in the same CRS');
201
            }
202
203
            /* @var GeographicPoint $to */
204 144
            return static::vincenty($this->asGeographicValue(), $to->asGeographicValue(), $this->getCRS()->getDatum()->getEllipsoid());
205
        }
206
    }
207
208 36
    public function __toString(): string
209
    {
210 36
        $values = [];
211 36
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
212 36
            if ($axis->getName() === Axis::GEODETIC_LATITUDE) {
213 36
                $values[] = $this->latitude;
214 36
            } elseif ($axis->getName() === Axis::GEODETIC_LONGITUDE) {
215 36
                $values[] = $this->longitude;
216 9
            } elseif ($axis->getName() === Axis::ELLIPSOIDAL_HEIGHT) {
217 9
                $values[] = $this->height;
218
            } else {
219
                throw new UnknownAxisException(); // @codeCoverageIgnore
220
            }
221
        }
222
223 36
        return '(' . implode(', ', $values) . ')';
224
    }
225
226
    /**
227
     * Geographic/geocentric conversions
228
     * 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
        Geocentric $to
233
    ): GeocentricPoint {
234 9
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
235 9
        $asGeocentric = $geographicValue->asGeocentricValue();
236
237 9
        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
     * 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 54
    public function coordinateFrameRotation(
247
        Geographic $to,
248
        Length $xAxisTranslation,
249
        Length $yAxisTranslation,
250
        Length $zAxisTranslation,
251
        Angle $xAxisRotation,
252
        Angle $yAxisRotation,
253
        Angle $zAxisRotation,
254
        Scale $scaleDifference
255
    ): self {
256 54
        return $this->coordinateFrameMolodenskyBadekas(
257 54
            $to,
258
            $xAxisTranslation,
259
            $yAxisTranslation,
260
            $zAxisTranslation,
261
            $xAxisRotation,
262
            $yAxisRotation,
263
            $zAxisRotation,
264
            $scaleDifference,
265 54
            new Metre(0),
266 54
            new Metre(0),
267 54
            new Metre(0)
268
        );
269
    }
270
271
    /**
272
     * Molodensky-Badekas (CF geog2D/geog3D domain)
273
     * 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 72
    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
        Length $ordinate2OfEvaluationPoint,
287
        Length $ordinate3OfEvaluationPoint
288
    ): self {
289 72
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
290 72
        $asGeocentric = $geographicValue->asGeocentricValue();
291
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 72
        $xp = $ordinate1OfEvaluationPoint->asMetres()->getValue();
303 72
        $yp = $ordinate2OfEvaluationPoint->asMetres()->getValue();
304 72
        $zp = $ordinate3OfEvaluationPoint->asMetres()->getValue();
305
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 72
        $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 72
        $newGeographic = $newGeocentric->asGeographicValue();
311
312 72
        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
     * 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 171
    public function positionVectorTransformation(
322
        Geographic $to,
323
        Length $xAxisTranslation,
324
        Length $yAxisTranslation,
325
        Length $zAxisTranslation,
326
        Angle $xAxisRotation,
327
        Angle $yAxisRotation,
328
        Angle $zAxisRotation,
329
        Scale $scaleDifference
330
    ): self {
331 171
        return $this->positionVectorMolodenskyBadekas(
332 171
            $to,
333
            $xAxisTranslation,
334
            $yAxisTranslation,
335
            $zAxisTranslation,
336
            $xAxisRotation,
337
            $yAxisRotation,
338
            $zAxisRotation,
339
            $scaleDifference,
340 171
            new Metre(0),
341 171
            new Metre(0),
342 171
            new Metre(0)
343
        );
344
    }
345
346
    /**
347
     * Molodensky-Badekas (PV geog2D/geog3D domain)
348
     * 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 189
    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
        Length $ordinate2OfEvaluationPoint,
362
        Length $ordinate3OfEvaluationPoint
363
    ): self {
364 189
        $geographicValue = new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
365 189
        $asGeocentric = $geographicValue->asGeocentricValue();
366
367 189
        $xs = $asGeocentric->getX()->asMetres()->getValue();
368 189
        $ys = $asGeocentric->getY()->asMetres()->getValue();
369 189
        $zs = $asGeocentric->getZ()->asMetres()->getValue();
370 189
        $tx = $xAxisTranslation->asMetres()->getValue();
371 189
        $ty = $yAxisTranslation->asMetres()->getValue();
372 189
        $tz = $zAxisTranslation->asMetres()->getValue();
373 189
        $rx = $xAxisRotation->asRadians()->getValue();
374 189
        $ry = $yAxisRotation->asRadians()->getValue();
375 189
        $rz = $zAxisRotation->asRadians()->getValue();
376 189
        $M = 1 + $scaleDifference->asUnity()->getValue();
377 189
        $xp = $ordinate1OfEvaluationPoint->asMetres()->getValue();
378 189
        $yp = $ordinate2OfEvaluationPoint->asMetres()->getValue();
379 189
        $zp = $ordinate3OfEvaluationPoint->asMetres()->getValue();
380
381 189
        $xt = $M * ((($xs - $xp) * 1) + (($ys - $yp) * -$rz) + (($zs - $zp) * $ry)) + $tx + $xp;
382 189
        $yt = $M * ((($xs - $xp) * $rz) + (($ys - $yp) * 1) + (($zs - $zp) * -$rx)) + $ty + $yp;
383 189
        $zt = $M * ((($xs - $xp) * -$ry) + (($ys - $yp) * $rx) + (($zs - $zp) * 1)) + $tz + $zp;
384 189
        $newGeocentric = new GeocentricValue(new Metre($xt), new Metre($yt), new Metre($zt), $to->getDatum());
385 189
        $newGeographic = $newGeocentric->asGeographicValue();
386
387 189
        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
     * 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 63
    public function geocentricTranslation(
397
        Geographic $to,
398
        Length $xAxisTranslation,
399
        Length $yAxisTranslation,
400
        Length $zAxisTranslation
401
    ): self {
402 63
        return $this->positionVectorTransformation(
403 63
            $to,
404
            $xAxisTranslation,
405
            $yAxisTranslation,
406
            $zAxisTranslation,
407 63
            new Radian(0),
408 63
            new Radian(0),
409 63
            new Radian(0),
410 63
            new Unity(0)
411
        );
412
    }
413
414
    /**
415
     * Abridged Molodensky
416
     * 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 18
    public function abridgedMolodensky(
420
        Geographic $to,
421
        Length $xAxisTranslation,
422
        Length $yAxisTranslation,
423
        Length $zAxisTranslation,
424
        Length $differenceInSemiMajorAxis,
425
        Scale $differenceInFlattening
426
    ): 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 18
        $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 18
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
441
442 18
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
443
444 18
        $dLatitude = ((-$tx * sin($latitude) * cos($longitude)) - ($ty * sin($latitude) * sin($longitude)) + ($tz * cos($latitude)) + ((($a * $df) + ($this->crs->getDatum()->getEllipsoid()->getInverseFlattening() * $da)) * sin(2 * $latitude))) / ($rho * sin((new ArcSecond(1))->asRadians()->getValue()));
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
448 18
        $toLatitude = $latitude + (new ArcSecond($dLatitude))->asRadians()->getValue();
449 18
        $toLongitude = $longitude + (new ArcSecond($dLongitude))->asRadians()->getValue();
450 18
        $toHeight = $fromHeight + $dHeight;
451
452 18
        return static::create(new Radian($toLatitude), new Radian($toLongitude), $to instanceof Geographic3D ? new Metre($toHeight) : null, $to, $this->epoch);
453
    }
454
455
    /**
456
     * Molodensky
457
     * See Abridged Molodensky.
458
     */
459 18
    public function molodensky(
460
        Geographic $to,
461
        Length $xAxisTranslation,
462
        Length $yAxisTranslation,
463
        Length $zAxisTranslation,
464
        Length $differenceInSemiMajorAxis,
465
        Scale $differenceInFlattening
466
    ): 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 18
        $tz = $zAxisTranslation->asMetres()->getValue();
473 18
        $da = $differenceInSemiMajorAxis->asMetres()->getValue();
474 18
        $df = $differenceInFlattening->asUnity()->getValue();
475
476 18
        $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 18
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
482
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
485 18
        $dLatitude = ((-$tx * sin($latitude) * cos($longitude)) - ($ty * sin($latitude) * sin($longitude)) + ($tz * cos($latitude)) + ($da * ($nu * $e2 * sin($latitude) * cos($latitude)) / $a + $df * ($rho * ($a / $b) + $nu * ($b / $a)) * sin($latitude) * cos($latitude))) / (($rho + $fromHeight) * sin((new ArcSecond(1))->asRadians()->getValue()));
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
489 18
        $toLatitude = $latitude + (new ArcSecond($dLatitude))->asRadians()->getValue();
490 18
        $toLongitude = $longitude + (new ArcSecond($dLongitude))->asRadians()->getValue();
491 18
        $toHeight = $fromHeight + $dHeight;
492
493 18
        return static::create(new Radian($toLatitude), new Radian($toLongitude), $to instanceof Geographic3D ? new Metre($toHeight) : null, $to, $this->epoch);
494
    }
495
496
    /**
497
     * Albers Equal Area.
498
     */
499 18
    public function albersEqualArea(
500
        Projected $to,
501
        Angle $latitudeOfFalseOrigin,
502
        Angle $longitudeOfFalseOrigin,
503
        Angle $latitudeOf1stStandardParallel,
504
        Angle $latitudeOf2ndStandardParallel,
505
        Length $eastingAtFalseOrigin,
506
        Length $northingAtFalseOrigin
507
    ): 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 18
        $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
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 18
        $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
525 18
        $n = ($centralMeridianFirstParallel ** 2 - $centralMeridianSecondParallel ** 2) / ($alphaSecondParallel - $alphaFirstParallel);
526 18
        $C = $centralMeridianFirstParallel ** 2 + $n * $alphaFirstParallel;
527 18
        $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 18
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rhoOrigin - ($rho * cos($theta));
533
534 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
535
    }
536
537
    /**
538
     * American Polyconic.
539
     */
540 9
    public function americanPolyconic(
541
        Projected $to,
542
        Angle $latitudeOfNaturalOrigin,
543
        Angle $longitudeOfNaturalOrigin,
544
        Length $falseEasting,
545
        Length $falseNorthing
546
    ): 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 9
        $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 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));
557
558 9
        if ($latitude === 0.0) {
0 ignored issues
show
introduced by
The condition $latitude === 0.0 is always false.
Loading history...
559
            $easting = $falseEasting->asMetres()->getValue() + $a * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
560
            $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 9
            $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 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
570
    }
571
572
    /**
573
     * Bonne.
574
     */
575 9
    public function bonne(
576
        Projected $to,
577
        Angle $latitudeOfNaturalOrigin,
578
        Angle $longitudeOfNaturalOrigin,
579
        Length $falseEasting,
580
        Length $falseNorthing
581
    ): 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 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + (($a * $mO / sin($latitudeOrigin) - $rho * cos($tau)));
601
602 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
603
    }
604
605
    /**
606
     * Bonne South Orientated.
607
     */
608 9
    public function bonneSouthOrientated(
609
        Projected $to,
610
        Angle $latitudeOfNaturalOrigin,
611
        Angle $longitudeOfNaturalOrigin,
612
        Length $falseEasting,
613
        Length $falseNorthing
614
    ): 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 9
        $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 9
        $southing = $falseNorthing->asMetres()->getValue() - (($a * $mO / sin($latitudeOrigin) - $rho * cos($tau)));
634
635 9
        return ProjectedPoint::create(new Metre(-$westing), new Metre(-$southing), new Metre($westing), new Metre($southing), $to, $this->epoch);
636
    }
637
638
    /**
639
     * Cassini-Soldner.
640
     */
641 9
    public function cassiniSoldner(
642
        Projected $to,
643
        Angle $latitudeOfNaturalOrigin,
644
        Angle $longitudeOfNaturalOrigin,
645
        Length $falseEasting,
646
        Length $falseNorthing
647
    ): 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 9
        $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
659 9
        $A = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() * cos($latitude);
660 9
        $T = tan($latitude) ** 2;
661 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $X;
667
668 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
669
    }
670
671
    /**
672
     * Hyperbolic Cassini-Soldner.
673
     */
674 18
    public function hyperbolicCassiniSoldner(
675
        Projected $to,
676
        Angle $latitudeOfNaturalOrigin,
677
        Angle $longitudeOfNaturalOrigin,
678
        Length $falseEasting,
679
        Length $falseNorthing
680
    ): 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 18
        $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
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 18
        $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 18
        $northing = $falseNorthing->asMetres()->getValue() + $X - ($X ** 3 / (6 * $rho * $nu));
701
702 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
703
    }
704
705
    /**
706
     * Colombia Urban.
707
     */
708 9
    public function columbiaUrban(
709
        Projected $to,
710
        Angle $latitudeOfNaturalOrigin,
711
        Angle $longitudeOfNaturalOrigin,
712
        Length $falseEasting,
713
        Length $falseNorthing,
714
        Length $projectionPlaneOriginHeight
715
    ): ProjectedPoint {
716 9
        $latitude = $this->latitude->asRadians()->getValue();
717 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
718 9
        $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
728 9
        $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 9
        $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 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
736
    }
737
738
    /**
739
     * Equal Earth.
740
     */
741 9
    public function equalEarth(
742
        Projected $to,
743
        Angle $longitudeOfNaturalOrigin,
744
        Length $falseEasting,
745
        Length $falseNorthing
746
    ): ProjectedPoint {
747 9
        $latitude = $this->latitude->asRadians()->getValue();
748 9
        $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
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 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $Rq * $theta * (1.340264 - 0.081106 * $theta ** 2 + $theta ** 6 * (0.000893 + 0.003796 * $theta ** 2));
760
761 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
762
    }
763
764
    /**
765
     * Equidistant Cylindrical
766
     * See method code 1029 for spherical development. See also Pseudo Plate Carree, method code 9825.
767
     */
768 9
    public function equidistantCylindrical(
769
        Projected $to,
770
        Angle $latitudeOf1stStandardParallel,
771
        Angle $longitudeOfNaturalOrigin,
772
        Length $falseEasting,
773
        Length $falseNorthing
774
    ): 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 9
        $e10 = $e ** 10;
784 9
        $e12 = $e ** 12;
785 9
        $e14 = $e ** 14;
786
787 9
        $nu1 = $a / sqrt(1 - $e2 * sin($latitudeFirstParallel) ** 2);
788
789
        $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 9
            (-693 / 1310720 * $e10 - 6537 / 5242880 * $e12 - 297297 / 167772160 * $e14) * sin(10 * $latitude) +
796 9
            (1001 / 8388608 * $e12 + 11011 / 33554432 * $e14) * sin(12 * $latitude) +
797 9
            (-6435 / 234881024 * $e ** 14) * sin(14 * $latitude)
798
        );
799
800 9
        $easting = $falseEasting->asMetres()->getValue() + $nu1 * cos($latitudeFirstParallel) * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
801 9
        $northing = $falseNorthing->asMetres()->getValue() + $M;
802
803 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
804
    }
805
806
    /**
807
     * Guam Projection
808
     * Simplified form of Oblique Azimuthal Equidistant projection method.
809
     */
810 9
    public function guamProjection(
811
        Projected $to,
812
        Angle $latitudeOfNaturalOrigin,
813
        Angle $longitudeOfNaturalOrigin,
814
        Length $falseEasting,
815
        Length $falseNorthing
816
    ): 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 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
822 9
        $e4 = $e ** 4;
823 9
        $e6 = $e ** 6;
824
825 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));
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 9
        $northing = $falseNorthing->asMetres()->getValue() + $M - $MO + ($x ** 2 * tan($latitude) * sqrt(1 - $e2 * sin($latitude) ** 2) / (2 * $a));
831
832 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
833
    }
834
835
    /**
836
     * Krovak.
837
     */
838 36
    public function krovak(
839
        Projected $to,
840
        Angle $latitudeOfProjectionCentre,
841
        Angle $longitudeOfOrigin,
842
        Angle $coLatitudeOfConeAxis,
843
        Angle $latitudeOfPseudoStandardParallel,
844
        Scale $scaleFactorOnPseudoStandardParallel,
845
        Length $falseEasting,
846
        Length $falseNorthing
847
    ): 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 36
        $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
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 36
        $tO = tan(M_PI / 4 + $upsilonO / 2) * ((1 + $e * sin($latitudeC)) / (1 - $e * sin($latitudeC))) ** ($e * $B / 2) / (tan(M_PI / 4 + $latitudeC / 2) ** $B);
864 36
        $n = sin($latitudeP);
865 36
        $rO = $kP * $A / tan($latitudeP);
866
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 36
        $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 36
        $southing = $X + $falseNorthing->asMetres()->getValue();
878
879 36
        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
     * Incorporates a polynomial transformation which is defined to be exact and for practical purposes is considered
885
     * to be a map projection.
886
     */
887 18
    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
        Coefficient $C9,
907
        Coefficient $C10
908
    ): ProjectedPoint {
909 18
        $asKrovak = $this->krovak($to, $latitudeOfProjectionCentre, $longitudeOfOrigin, $coLatitudeOfConeAxis, $latitudeOfPseudoStandardParallel, $scaleFactorOnPseudoStandardParallel, new Metre(0), new Metre(0));
910
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 18
        $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 18
        $southing += $falseNorthing->asMetres()->getValue() - $dX;
932
933 18
        return ProjectedPoint::create(new Metre(-$westing), new Metre(-$southing), new Metre($westing), new Metre($southing), $to, $this->epoch);
934
    }
935
936
    /**
937
     * Lambert Azimuthal Equal Area
938
     * This is the ellipsoidal form of the projection.
939
     */
940 9
    public function lambertAzimuthalEqualArea(
941
        Projected $to,
942
        Angle $latitudeOfNaturalOrigin,
943
        Angle $longitudeOfNaturalOrigin,
944
        Length $falseEasting,
945
        Length $falseNorthing
946
    ): ProjectedPoint {
947 9
        $latitude = $this->latitude->asRadians()->getValue();
948 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
949 9
        $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
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 9
        $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 9
        $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 9
        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
     * (code 9820) for ellipsoidal form.  Differences of several tens of metres result from comparison of the two
972
     * methods.
973
     */
974 9
    public function lambertAzimuthalEqualAreaSpherical(
975
        Projected $to,
976
        Angle $latitudeOfNaturalOrigin,
977
        Angle $longitudeOfNaturalOrigin,
978
        Length $falseEasting,
979
        Length $falseNorthing
980
    ): ProjectedPoint {
981 9
        $latitude = $this->latitude->asRadians()->getValue();
982 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
983 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
984
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 9
        $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 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
991
    }
992
993
    /**
994
     * Lambert Conic Conformal (1SP).
995
     */
996 9
    public function lambertConicConformal1SP(
997
        Projected $to,
998
        Angle $latitudeOfNaturalOrigin,
999
        Angle $longitudeOfNaturalOrigin,
1000
        Scale $scaleFactorAtNaturalOrigin,
1001
        Length $falseEasting,
1002
        Length $falseNorthing
1003
    ): ProjectedPoint {
1004 9
        $latitude = $this->latitude->asRadians()->getValue();
1005 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1006 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1007 9
        $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
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 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $rO - $r * cos($theta);
1022
1023 9
        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
     * 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 9
    public function lambertConicConformal2SPBelgium(
1068
        Projected $to,
1069
        Angle $latitudeOfFalseOrigin,
1070
        Angle $longitudeOfFalseOrigin,
1071
        Angle $latitudeOf1stStandardParallel,
1072
        Angle $latitudeOf2ndStandardParallel,
1073
        Length $eastingAtFalseOrigin,
1074
        Length $northingAtFalseOrigin
1075
    ): 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 9
        $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
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 9
        $rF = $a * $F * $tF ** $n;
1094 9
        if (is_nan($rF)) {
1095 9
            $rF = 0;
1096
        }
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 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1101
1102 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1103
    }
1104
1105
    /**
1106
     * Lambert Conic Conformal (2SP Michigan).
1107
     */
1108 9
    public function lambertConicConformal2SPMichigan(
1109
        Projected $to,
1110
        Angle $latitudeOfFalseOrigin,
1111
        Angle $longitudeOfFalseOrigin,
1112
        Angle $latitudeOf1stStandardParallel,
1113
        Angle $latitudeOf2ndStandardParallel,
1114
        Length $eastingAtFalseOrigin,
1115
        Length $northingAtFalseOrigin,
1116
        Scale $ellipsoidScalingFactor
1117
    ): 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 9
        $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
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 9
        $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 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1141
1142 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1143
    }
1144
1145
    /**
1146
     * Lambert Conic Conformal (2SP).
1147
     */
1148 9
    public function lambertConicConformal2SP(
1149
        Projected $to,
1150
        Angle $latitudeOfFalseOrigin,
1151
        Angle $longitudeOfFalseOrigin,
1152
        Angle $latitudeOf1stStandardParallel,
1153
        Angle $latitudeOf2ndStandardParallel,
1154
        Length $eastingAtFalseOrigin,
1155
        Length $northingAtFalseOrigin
1156
    ): 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 9
        $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
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 9
        $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 9
        $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rF - $r * cos($theta);
1179
1180 9
        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
     * 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 9
    public function lambertConicNearConformal(
1222
        Projected $to,
1223
        Angle $latitudeOfNaturalOrigin,
1224
        Angle $longitudeOfNaturalOrigin,
1225
        Scale $scaleFactorAtNaturalOrigin,
1226
        Length $falseEasting,
1227
        Length $falseNorthing
1228
    ): ProjectedPoint {
1229 9
        $latitude = $this->latitude->asRadians()->getValue();
1230 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1231 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1232 9
        $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
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 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $M + $r * sin($theta) * tan($theta / 2);
1255
1256 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1257
    }
1258
1259
    /**
1260
     * Lambert Cylindrical Equal Area
1261
     * This is the ellipsoidal form of the projection.
1262
     */
1263 9
    public function lambertCylindricalEqualArea(
1264
        Projected $to,
1265
        Angle $latitudeOf1stStandardParallel,
1266
        Angle $longitudeOfNaturalOrigin,
1267
        Length $falseEasting,
1268
        Length $falseNorthing
1269
    ): ProjectedPoint {
1270 9
        $latitude = $this->latitude->asRadians()->getValue();
1271 9
        $latitudeFirstParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1272 9
        $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $y;
1284
1285 9
        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
     * 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 9
    public function modifiedAzimuthalEquidistant(
1294
        Projected $to,
1295
        Angle $latitudeOfNaturalOrigin,
1296
        Angle $longitudeOfNaturalOrigin,
1297
        Length $falseEasting,
1298
        Length $falseNorthing
1299
    ): ProjectedPoint {
1300 9
        $latitude = $this->latitude->asRadians()->getValue();
1301 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1302 9
        $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
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 9
        $alpha = atan2(sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()), (cos($latitudeOrigin) * tan($psi) - sin($latitudeOrigin) * cos($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue())));
1310 9
        $G = $e * sin($latitudeOrigin) / sqrt(1 - $e2);
1311 9
        $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
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 9
        $northing = $falseNorthing->asMetres()->getValue() + $c * cos($alpha);
1323
1324 9
        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
     * 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 9
    public function obliqueStereographic(
1333
        Projected $to,
1334
        Angle $latitudeOfNaturalOrigin,
1335
        Angle $longitudeOfNaturalOrigin,
1336
        Scale $scaleFactorAtNaturalOrigin,
1337
        Length $falseEasting,
1338
        Length $falseNorthing
1339
    ): 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 9
        $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
1348 9
        $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
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 9
        $c = (($n + sin($latitudeOrigin)) * (1 - ($w1 - 1) / ($w1 + 1))) / (($n - sin($latitudeOrigin)) * (1 + ($w1 - 1) / ($w1 + 1)));
1357 9
        $w2 = $c * $w1;
1358 9
        $chiOrigin = self::asin(($w2 - 1) / ($w2 + 1));
1359
1360 9
        $lambda = $n * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue() + $longitudeOrigin;
1361
1362 9
        $Sa = (1 + sin($latitude)) / (1 - sin($latitude));
1363 9
        $Sb = (1 - $e * sin($latitude)) / (1 + $e * sin($latitude));
1364 9
        $w = $c * ($Sa * ($Sb ** $e)) ** $n;
1365 9
        $chi = self::asin(($w - 1) / ($w + 1));
1366
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 9
        $northing = $falseNorthing->asMetres()->getValue() + 2 * $R * $kO * (sin($chi) * cos($chiOrigin) - cos($chi) * sin($chiOrigin) * cos($lambda - $longitudeOrigin)) / $B;
1371
1372 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1373
    }
1374
1375
    /**
1376
     * 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 9
    public function polarStereographicVariantA(
1380
        Projected $to,
1381
        Angle $latitudeOfNaturalOrigin,
1382
        Angle $longitudeOfNaturalOrigin,
1383
        Scale $scaleFactorAtNaturalOrigin,
1384
        Length $falseEasting,
1385
        Length $falseNorthing
1386
    ): ProjectedPoint {
1387 9
        $latitude = $this->latitude->asRadians()->getValue();
1388 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1389 9
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1390 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1391 9
        $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
        } else {
1396 9
            $t = tan(M_PI / 4 - $latitude / 2) * (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1397
        }
1398 9
        $rho = 2 * $a * $kO * $t / sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e));
1399
1400 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1401 9
        $dE = $rho * sin($theta);
1402 9
        $dN = $rho * cos($theta);
1403
1404 9
        $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 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1412
    }
1413
1414
    /**
1415
     * Polar Stereographic (variant B).
1416
     */
1417 9
    public function polarStereographicVariantB(
1418
        Projected $to,
1419
        Angle $latitudeOfStandardParallel,
1420
        Angle $longitudeOfOrigin,
1421
        Length $falseEasting,
1422
        Length $falseNorthing
1423
    ): ProjectedPoint {
1424 9
        $latitude = $this->latitude->asRadians()->getValue();
1425 9
        $firstStandardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1426 9
        $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
1430 9
        if ($firstStandardParallel < 0) {
1431 9
            $tF = tan(M_PI / 4 + $firstStandardParallel / 2) / (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1432 9
            $t = tan(M_PI / 4 + $latitude / 2) / (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1433
        } else {
1434
            $tF = tan(M_PI / 4 - $firstStandardParallel / 2) * (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1435
            $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 9
        $kO = $mF * sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e)) / (2 * $tF);
1439
1440 9
        $rho = 2 * $a * $kO * $t / sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e));
1441
1442 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfOrigin))->asRadians()->getValue();
1443 9
        $dE = $rho * sin($theta);
1444 9
        $dN = $rho * cos($theta);
1445
1446 9
        $easting = $falseEasting->asMetres()->getValue() + $dE;
1447 9
        if ($firstStandardParallel < 0) {
1448 9
            $northing = $falseNorthing->asMetres()->getValue() + $dN;
1449
        } else {
1450
            $northing = $falseNorthing->asMetres()->getValue() - $dN;
1451
        }
1452
1453 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1454
    }
1455
1456
    /**
1457
     * Polar Stereographic (variant C).
1458
     */
1459 9
    public function polarStereographicVariantC(
1460
        Projected $to,
1461
        Angle $latitudeOfStandardParallel,
1462
        Angle $longitudeOfOrigin,
1463
        Length $eastingAtFalseOrigin,
1464
        Length $northingAtFalseOrigin
1465
    ): ProjectedPoint {
1466 9
        $latitude = $this->latitude->asRadians()->getValue();
1467 9
        $firstStandardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1468 9
        $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
1472 9
        if ($firstStandardParallel < 0) {
1473 9
            $tF = tan(M_PI / 4 + $firstStandardParallel / 2) / (((1 + $e * sin($firstStandardParallel)) / (1 - $e * sin($firstStandardParallel))) ** ($e / 2));
1474 9
            $t = tan(M_PI / 4 + $latitude / 2) / (((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2));
1475
        } else {
1476
            $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
        }
1479 9
        $mF = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1480
1481 9
        $rhoF = $a * $mF;
1482 9
        $rho = $rhoF * $t / $tF;
1483
1484 9
        $theta = $this->normaliseLongitude($this->longitude->subtract($longitudeOfOrigin))->asRadians()->getValue();
1485 9
        $dE = $rho * sin($theta);
1486 9
        $dN = $rho * cos($theta);
1487
1488 9
        $easting = $eastingAtFalseOrigin->asMetres()->getValue() + $dE;
1489 9
        if ($firstStandardParallel < 0) {
1490 9
            $northing = $northingAtFalseOrigin->asMetres()->getValue() - $rhoF + $dN;
1491
        } else {
1492
            $northing = $northingAtFalseOrigin->asMetres()->getValue() + $rhoF - $dN;
1493
        }
1494
1495 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1496
    }
1497
1498
    /**
1499
     * 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 9
    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
        Length $falseEasting,
1507
        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 9
        $northing = $falseNorthing->asMetres()->getValue() + $a * log(tan(M_PI / 4 + $latitude / 2));
1514
1515 9
        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
     * 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 18
    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
        Length $falseEasting,
1530
        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 18
        $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 18
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1542
    }
1543
1544
    /**
1545
     * Mercator (variant B)
1546
     * Used for most nautical charts.
1547
     */
1548 9
    public function mercatorVariantB(
1549
        Projected $to,
1550
        Angle $latitudeOf1stStandardParallel,
1551
        Angle $longitudeOfNaturalOrigin,
1552
        Length $falseEasting,
1553
        Length $falseNorthing
1554
    ): ProjectedPoint {
1555 9
        $latitude = $this->latitude->asRadians()->getValue();
1556 9
        $firstStandardParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1557 9
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1558 9
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1559 9
        $e2 = $this->crs->getDatum()->getEllipsoid()->getEccentricitySquared();
1560
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 9
        $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 9
        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
     * 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 9
    public function longitudeRotation(
1575
        Geographic $to,
1576
        Angle $longitudeOffset
1577
    ): self {
1578 9
        $newLongitude = $this->longitude->add($longitudeOffset);
1579
1580 9
        return static::create($this->latitude, $newLongitude, $this->height, $to, $this->epoch);
1581
    }
1582
1583
    /**
1584
     * Hotine Oblique Mercator (variant A).
1585
     */
1586 9
    public function obliqueMercatorHotineVariantA(
1587
        Projected $to,
1588
        Angle $latitudeOfProjectionCentre,
1589
        Angle $longitudeOfProjectionCentre,
1590
        Angle $azimuthOfInitialLine,
1591
        Angle $angleFromRectifiedToSkewGrid,
1592
        Scale $scaleFactorOnInitialLine,
1593
        Length $falseEasting,
1594
        Length $falseNorthing
1595
    ): 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 9
        $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
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 9
        $G = ($F - 1 / $F) / 2;
1615 9
        $gammaO = self::asin(sin($alphaC) / $D);
1616 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1617
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 9
        $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 9
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $falseNorthing->asMetres()->getValue();
1629
1630 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1631
    }
1632
1633
    /**
1634
     * Hotine Oblique Mercator (variant B).
1635
     */
1636 9
    public function obliqueMercatorHotineVariantB(
1637
        Projected $to,
1638
        Angle $latitudeOfProjectionCentre,
1639
        Angle $longitudeOfProjectionCentre,
1640
        Angle $azimuthOfInitialLine,
1641
        Angle $angleFromRectifiedToSkewGrid,
1642
        Scale $scaleFactorOnInitialLine,
1643
        Length $eastingAtProjectionCentre,
1644
        Length $northingAtProjectionCentre
1645
    ): 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 9
        $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
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 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1667 9
        $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
        }
1673
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 9
        $V = sin($B * ($longitude - $lonO));
1679 9
        $U = (-$V * cos($gammaO) + $S * sin($gammaO)) / $T;
1680 9
        $v = $A * log((1 - $U) / (1 + $U)) / (2 * $B);
1681
1682 9
        if ($alphaC === M_PI / 2) {
1683
            if ($longitude === $lonC) {
1684
                $u = 0;
1685
            } else {
1686
                $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
        }
1691
1692 9
        $easting = $v * cos($gammaC) + $u * sin($gammaC) + $eastingAtProjectionCentre->asMetres()->getValue();
1693 9
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $northingAtProjectionCentre->asMetres()->getValue();
1694
1695 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1696
    }
1697
1698
    /**
1699
     * Laborde Oblique Mercator.
1700
     */
1701 9
    public function obliqueMercatorLaborde(
1702
        Projected $to,
1703
        Angle $latitudeOfProjectionCentre,
1704
        Angle $longitudeOfProjectionCentre,
1705
        Angle $azimuthOfInitialLine,
1706
        Scale $scaleFactorOnInitialLine,
1707
        Length $falseEasting,
1708
        Length $falseNorthing
1709
    ): 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 9
        $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
1718 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1719 9
        $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
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 9
        $W = cos($P) * sin($L);
1729 9
        $d = hypot($U, $V);
1730 9
        if ($d === 0.0) {
1731
            $LPrime = 0;
1732
            $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 9
        $northing = $falseNorthing->asMetres()->getValue() + $R * $H->pow(3)->multiply($G)->add($H)->getReal();
1742
1743 9
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1744
    }
1745
1746
    /**
1747
     * Transverse Mercator.
1748
     */
1749 108
    public function transverseMercator(
1750
        Projected $to,
1751
        Angle $latitudeOfNaturalOrigin,
1752
        Angle $longitudeOfNaturalOrigin,
1753
        Scale $scaleFactorAtNaturalOrigin,
1754
        Length $falseEasting,
1755
        Length $falseNorthing
1756
    ): ProjectedPoint {
1757 108
        $latitude = $this->latitude->asRadians()->getValue();
1758 108
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1759 108
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1760 108
        $a = $this->crs->getDatum()->getEllipsoid()->getSemiMajorAxis()->asMetres()->getValue();
1761 108
        $e = $this->crs->getDatum()->getEllipsoid()->getEccentricity();
1762 108
        $f = $this->crs->getDatum()->getEllipsoid()->getInverseFlattening();
1763
1764 108
        $n = $f / (2 - $f);
1765 108
        $B = ($a / (1 + $n)) * (1 + $n ** 2 / 4 + $n ** 4 / 64);
1766
1767 108
        $h1 = $n / 2 - (2 / 3) * $n ** 2 + (5 / 16) * $n ** 3 + (41 / 180) * $n ** 4;
1768 108
        $h2 = (13 / 48) * $n ** 2 - (3 / 5) * $n ** 3 + (557 / 1440) * $n ** 4;
1769 108
        $h3 = (61 / 240) * $n ** 3 - (103 / 140) * $n ** 4;
1770 108
        $h4 = (49561 / 161280) * $n ** 4;
1771
1772 108
        if ($latitudeOrigin === 0.0) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === 0.0 is always false.
Loading history...
1773 81
            $mO = 0;
1774 27
        } elseif ($latitudeOrigin === M_PI / 2) {
1775
            $mO = $B * M_PI / 2;
1776 27
        } elseif ($latitudeOrigin === -M_PI / 2) {
1777
            $mO = $B * -M_PI / 2;
1778
        } else {
1779 27
            $qO = asinh(tan($latitudeOrigin)) - ($e * atanh($e * sin($latitudeOrigin)));
1780 27
            $betaO = atan(sinh($qO));
1781 27
            $xiO0 = self::asin(sin($betaO));
1782 27
            $xiO1 = $h1 * sin(2 * $xiO0);
1783 27
            $xiO2 = $h2 * sin(4 * $xiO0);
1784 27
            $xiO3 = $h3 * sin(6 * $xiO0);
1785 27
            $xiO4 = $h4 * sin(8 * $xiO0);
1786 27
            $xiO = $xiO0 + $xiO1 + $xiO2 + $xiO3 + $xiO4;
1787 27
            $mO = $B * $xiO;
1788
        }
1789
1790 108
        $Q = asinh(tan($latitude)) - ($e * atanh($e * sin($latitude)));
1791 108
        $beta = atan(sinh($Q));
1792 108
        $eta0 = atanh(cos($beta) * sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()));
1793 108
        $xi0 = self::asin(sin($beta) * cosh($eta0));
1794 108
        $xi1 = $h1 * sin(2 * $xi0) * cosh(2 * $eta0);
1795 108
        $eta1 = $h1 * cos(2 * $xi0) * sinh(2 * $eta0);
1796 108
        $xi2 = $h2 * sin(4 * $xi0) * cosh(4 * $eta0);
1797 108
        $eta2 = $h2 * cos(4 * $xi0) * sinh(4 * $eta0);
1798 108
        $xi3 = $h3 * sin(6 * $xi0) * cosh(6 * $eta0);
1799 108
        $eta3 = $h3 * cos(6 * $xi0) * sinh(6 * $eta0);
1800 108
        $xi4 = $h4 * sin(8 * $xi0) * cosh(8 * $eta0);
1801 108
        $eta4 = $h4 * cos(8 * $xi0) * sinh(8 * $eta0);
1802 108
        $xi = $xi0 + $xi1 + $xi2 + $xi3 + $xi4;
1803 108
        $eta = $eta0 + $eta1 + $eta2 + $eta3 + $eta4;
1804
1805 108
        $easting = $falseEasting->asMetres()->getValue() + $kO * $B * $eta;
1806 108
        $northing = $falseNorthing->asMetres()->getValue() + $kO * ($B * $xi - $mO);
1807
1808 108
        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
     * If locations fall outwith the fixed zones the general Transverse Mercator method (code 9807) must be used for
1814
     * each zone.
1815
     */
1816 36
    public function transverseMercatorZonedGrid(
1817
        Projected $to,
1818
        Angle $latitudeOfNaturalOrigin,
1819
        Angle $initialLongitude,
1820
        Angle $zoneWidth,
1821
        Scale $scaleFactorAtNaturalOrigin,
1822
        Length $falseEasting,
1823
        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 36
        $falseEasting = $falseEasting->add(new Metre($Z * 1000000));
1830
1831 36
        return $this->transverseMercator($to, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
1832
    }
1833
1834
    /**
1835
     * New Zealand Map Grid.
1836
     */
1837 27
    public function newZealandMapGrid(
1838
        Projected $to,
1839
        Angle $latitudeOfNaturalOrigin,
1840
        Angle $longitudeOfNaturalOrigin,
1841
        Length $falseEasting,
1842
        Length $falseNorthing
1843
    ): 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
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 27
        $deltaPsi += -0.001333 * ($deltaLatitudeToOrigin * 0.00001) ** 8;
1858 27
        $deltaPsi += 0.00067 * ($deltaLatitudeToOrigin * 0.00001) ** 9;
1859 27
        $deltaPsi += -0.00034 * ($deltaLatitudeToOrigin * 0.00001) ** 10;
1860
1861 27
        $zeta = new ComplexNumber($deltaPsi, $deltaLongitudeToOrigin->getValue());
1862
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 27
        $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 27
        $northing = $falseNorthing->asMetres()->getValue() + $z->getReal() * $a;
1879
1880 27
        return ProjectedPoint::create(new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $to, $this->epoch);
1881
    }
1882
1883
    /**
1884
     * Madrid to ED50 polynomial.
1885
     */
1886 9
    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
        Scale $B2,
1896
        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 9
        $dLongitude = $B00->add(new ArcSecond($B0->add($B1->multiply($this->latitude->getValue()))->add($B2->multiply($this->longitude->getValue()))->add($B3->multiply($this->height ? $this->height->getValue() : 0))->getValue()));
1900
1901 9
        return self::create($this->latitude->add($dLatitude), $this->longitude->add($dLongitude), null, $to, $this->epoch);
1902
    }
1903
1904
    /**
1905
     * Geographic3D to 2D conversion.
1906
     */
1907 45
    public function threeDToTwoD(
1908
        Geographic $to
1909
    ): self {
1910 45
        if ($to instanceof Geographic2D) {
1911 45
            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
     * 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 9
    public function geographic2DOffsets(
1923
        Geographic $to,
1924
        Angle $latitudeOffset,
1925
        Angle $longitudeOffset
1926
    ): self {
1927 9
        $toLatitude = $this->latitude->add($latitudeOffset);
1928 9
        $toLongitude = $this->longitude->add($longitudeOffset);
1929
1930 9
        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
     * General polynomial.
1956
     * @param Coefficient[] $powerCoefficients
1957
     */
1958 18
    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
        Scale $B0,
1968
        array $powerCoefficients
1969
    ): self {
1970 18
        $xs = $this->latitude->getValue();
1971 18
        $ys = $this->longitude->getValue();
1972
1973 18
        $t = $this->generalPolynomialUnitless(
1974 18
            $xs,
1975
            $ys,
1976
            $ordinate1OfEvaluationPointInSourceCRS,
1977
            $ordinate2OfEvaluationPointInSourceCRS,
1978
            $ordinate1OfEvaluationPointInTargetCRS,
1979
            $ordinate2OfEvaluationPointInTargetCRS,
1980
            $scalingFactorForSourceCRSCoordDifferences,
1981
            $scalingFactorForTargetCRSCoordDifferences,
1982
            $A0,
1983
            $B0,
1984
            $powerCoefficients
1985
        );
1986
1987 18
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
1988 18
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1989 18
            $xtUnit = Angle::EPSG_DEGREE;
1990
        }
1991 18
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
1992 18
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
1993 18
            $ytUnit = Angle::EPSG_DEGREE;
1994
        }
1995
1996 18
        return static::create(
1997 18
            Angle::makeUnit($t['xt'], $xtUnit),
1998 18
            Angle::makeUnit($t['yt'], $ytUnit),
1999 18
            $this->height,
2000
            $to,
2001 18
            $this->epoch
2002
        );
2003
    }
2004
2005
    /**
2006
     * Reversible polynomial.
2007
     * @param Coefficient[] $powerCoefficients
2008
     */
2009 36
    public function reversiblePolynomial(
2010
        Geographic $to,
2011
        Angle $ordinate1OfEvaluationPoint,
2012
        Angle $ordinate2OfEvaluationPoint,
2013
        Scale $scalingFactorForCoordDifferences,
2014
        Scale $A0,
2015
        Scale $B0,
2016
        $powerCoefficients
2017
    ): self {
2018 36
        $xs = $this->latitude->getValue();
2019 36
        $ys = $this->longitude->getValue();
2020
2021 36
        $t = $this->reversiblePolynomialUnitless(
2022 36
            $xs,
2023
            $ys,
2024
            $ordinate1OfEvaluationPoint,
2025
            $ordinate2OfEvaluationPoint,
2026
            $scalingFactorForCoordDifferences,
2027
            $A0,
2028
            $B0,
2029
            $powerCoefficients
2030
        );
2031
2032 36
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2033 36
        if ($xtUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2034 36
            $xtUnit = Angle::EPSG_DEGREE;
2035
        }
2036 36
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2037 36
        if ($ytUnit === Angle::EPSG_DEGREE_SUPPLIER_TO_DEFINE_REPRESENTATION) {
2038 36
            $ytUnit = Angle::EPSG_DEGREE;
2039
        }
2040
2041 36
        return static::create(
2042 36
            Angle::makeUnit($t['xt'], $xtUnit),
2043 36
            Angle::makeUnit($t['yt'], $ytUnit),
2044 36
            $this->height,
2045
            $to,
2046 36
            $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
        return static::create($this->latitude, $this->longitude, $this->height, $to, $this->epoch);
2058
    }
2059
2060
    /**
2061
     * Ordnance Survey National Transformation
2062
     * 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
     */
2065
    public function OSTN15(
2066
        Projected $to,
2067
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2068
    ): ProjectedPoint {
2069
        $etrs89NationalGrid = new Projected(
2070
            'ETRS89 / National Grid',
2071
            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
            $to->getBoundingArea()
2074
        );
2075
2076
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2077
2078
        return $eastingAndNorthingDifferenceFile->applyForwardAdjustment($projected);
2079
    }
2080
2081 342
    public function asGeographicValue(): GeographicValue
2082
    {
2083 342
        return new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
2084
    }
2085
2086 18
    public function asUTMPoint(): UTMPoint
2087
    {
2088 18
        $hemisphere = $this->getLatitude()->asDegrees()->getValue() >= 0 ? UTMPoint::HEMISPHERE_NORTH : UTMPoint::HEMISPHERE_SOUTH;
2089 18
        $latitudeOfNaturalOrigin = new Degree(0);
2090 18
        $initialLongitude = new Degree(-180);
2091 18
        $scaleFactorAtNaturalOrigin = new Unity(0.9996);
2092 18
        $falseEasting = new Metre(500000);
2093 18
        $falseNorthing = $hemisphere === UTMPoint::HEMISPHERE_NORTH ? new Metre(0) : new Metre(10000000);
2094 18
        $Z = ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / 6) % (360 / 6) + 1;
2095 18
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * 6 - 3));
2096
2097 18
        $projectedCRS = new Projected(
2098 18
            'UTM/' . $this->crs->getSRID(),
2099 18
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2100 18
            $this->crs->getDatum(),
2101 18
            BoundingArea::createWorld() // this is a dummy CRS for the transform only, details don't matter
2102
        );
2103
2104 18
        $asProjected = $this->transverseMercator($projectedCRS, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
2105
2106 18
        return new UTMPoint($asProjected->getEasting(), $asProjected->getNorthing(), $Z, $hemisphere, $this->crs, $this->epoch);
2107
    }
2108
}
2109