Passed
Push — master ( e5f776...37af97 )
by Doug
63:28
created

GeographicPoint::generalPolynomial()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 23
nc 1
nop 11
dl 0
loc 40
rs 9.552
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

1562
        /** @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...
1563
        Angle $longitudeOfNaturalOrigin,
1564
        Length $falseEasting,
1565
        Length $falseNorthing
1566
    ): ProjectedPoint {
1567
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1568
        $latitude = $this->latitude->asRadians()->getValue();
1569
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1570
1571
        $easting = $falseEasting->asMetres()->getValue() + $a * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1572
        $northing = $falseNorthing->asMetres()->getValue() + $a * log(tan(M_PI / 4 + $latitude / 2));
1573
1574
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1575
    }
1576
1577
    /**
1578
     * Mercator (variant A)
1579
     * Note that in these formulas the parameter latitude of natural origin (latO) is not used. However for this
1580
     * Mercator (variant A) method the EPSG dataset includes this parameter, which must have a value of zero, for
1581
     * completeness in CRS labelling.
1582
     */
1583
    public function mercatorVariantA(
1584
        Projected $to,
1585
        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

1585
        /** @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...
1586
        Angle $longitudeOfNaturalOrigin,
1587
        Scale $scaleFactorAtNaturalOrigin,
1588
        Length $falseEasting,
1589
        Length $falseNorthing
1590
    ): ProjectedPoint {
1591
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1592
        $latitude = $this->latitude->asRadians()->getValue();
1593
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1594
1595
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1596
        $e = $ellipsoid->getEccentricity();
1597
1598
        $easting = $falseEasting->asMetres()->getValue() + $a * $kO * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1599
        $northing = $falseNorthing->asMetres()->getValue() + $a * $kO * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1600
1601
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1602
    }
1603
1604
    /**
1605
     * Mercator (variant B)
1606
     * Used for most nautical charts.
1607
     */
1608
    public function mercatorVariantB(
1609
        Projected $to,
1610
        Angle $latitudeOf1stStandardParallel,
1611
        Angle $longitudeOfNaturalOrigin,
1612
        Length $falseEasting,
1613
        Length $falseNorthing
1614
    ): ProjectedPoint {
1615
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1616
        $latitude = $this->latitude->asRadians()->getValue();
1617
        $firstStandardParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1618
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1619
        $e = $ellipsoid->getEccentricity();
1620
        $e2 = $ellipsoid->getEccentricitySquared();
1621
1622
        $kO = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1623
1624
        $easting = $falseEasting->asMetres()->getValue() + $a * $kO * $this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue();
1625
        $northing = $falseNorthing->asMetres()->getValue() + $a * $kO * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1626
1627
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1628
    }
1629
1630
    /**
1631
     * Longitude rotation
1632
     * This transformation allows calculation of the longitude of a point in the target system by adding the parameter
1633
     * value to the longitude value of the point in the source system.
1634
     */
1635
    public function longitudeRotation(
1636
        Geographic2D|Geographic3D $to,
1637
        Angle $longitudeOffset
1638
    ): self {
1639
        $newLongitude = $this->longitude->add($longitudeOffset);
1640
1641
        return static::create($to, $this->latitude, $newLongitude, $this->height, $this->epoch);
1642
    }
1643
1644
    /**
1645
     * Hotine Oblique Mercator (variant A).
1646
     */
1647
    public function obliqueMercatorHotineVariantA(
1648
        Projected $to,
1649
        Angle $latitudeOfProjectionCentre,
1650
        Angle $longitudeOfProjectionCentre,
1651
        Angle $azimuthAtProjectionCentre,
1652
        Angle $angleFromRectifiedToSkewGrid,
1653
        Scale $scaleFactorAtProjectionCentre,
1654
        Length $falseEasting,
1655
        Length $falseNorthing
1656
    ): ProjectedPoint {
1657
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1658
        $latitude = $this->latitude->asRadians()->getValue();
1659
        $longitude = $this->longitude->asRadians()->getValue();
1660
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1661
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1662
        $alphaC = $azimuthAtProjectionCentre->asRadians()->getValue();
1663
        $kC = $scaleFactorAtProjectionCentre->asUnity()->getValue();
1664
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1665
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1666
        $e = $ellipsoid->getEccentricity();
1667
        $e2 = $ellipsoid->getEccentricitySquared();
1668
1669
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1670
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1671
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1672
        $D = $B * sqrt(1 - $e2) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1673
        $DD = max(1, $D ** 2);
1674
        $F = $D + sqrt($DD - 1) * static::sign($latC);
1675
        $H = $F * $tO ** $B;
1676
        $G = ($F - 1 / $F) / 2;
1677
        $gammaO = self::asin(sin($alphaC) / $D);
1678
        $lonO = $lonC - self::asin($G * tan($gammaO)) / $B;
1679
1680
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1681
        $Q = $H / $t ** $B;
1682
        $S = ($Q - 1 / $Q) / 2;
1683
        $T = ($Q + 1 / $Q) / 2;
1684
        $V = sin($B * ($longitude - $lonO));
1685
        $U = (-$V * cos($gammaO) + $S * sin($gammaO)) / $T;
1686
        $v = $A * log((1 - $U) / (1 + $U)) / (2 * $B);
1687
        $u = $A * atan2($S * cos($gammaO) + $V * sin($gammaO), cos($B * ($longitude - $lonO))) / $B;
1688
1689
        $easting = $v * cos($gammaC) + $u * sin($gammaC) + $falseEasting->asMetres()->getValue();
1690
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $falseNorthing->asMetres()->getValue();
1691
1692
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1693
    }
1694
1695
    /**
1696
     * Hotine Oblique Mercator (variant B).
1697
     */
1698
    public function obliqueMercatorHotineVariantB(
1699
        Projected $to,
1700
        Angle $latitudeOfProjectionCentre,
1701
        Angle $longitudeOfProjectionCentre,
1702
        Angle $azimuthAtProjectionCentre,
1703
        Angle $angleFromRectifiedToSkewGrid,
1704
        Scale $scaleFactorAtProjectionCentre,
1705
        Length $eastingAtProjectionCentre,
1706
        Length $northingAtProjectionCentre
1707
    ): ProjectedPoint {
1708
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1709
        $latitude = $this->latitude->asRadians()->getValue();
1710
        $longitude = $this->longitude->asRadians()->getValue();
1711
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1712
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1713
        $alphaC = $azimuthAtProjectionCentre->asRadians()->getValue();
1714
        $kC = $scaleFactorAtProjectionCentre->asUnity()->getValue();
1715
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1716
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1717
        $e = $ellipsoid->getEccentricity();
1718
        $e2 = $ellipsoid->getEccentricitySquared();
1719
1720
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1721
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1722
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1723
        $D = $B * sqrt(1 - $e2) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1724
        $F = $D + sqrt(max($D ** 2, 1) - 1) * static::sign($latC);
1725
        $H = $F * $tO ** $B;
1726
        $G = ($F - 1 / $F) / 2;
1727
        $gammaO = self::asin(sin($alphaC) / $D);
1728
        $lonO = $lonC - self::asin($G * tan($gammaO)) / $B;
1729
        $vC = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $vC is dead and can be removed.
Loading history...
1730
        if ($alphaC === M_PI / 2) {
1731
            $uC = $A * ($lonC - $lonO);
1732
        } else {
1733
            $uC = ($A / $B) * atan2(sqrt(max($D ** 2, 1) - 1), cos($alphaC)) * static::sign($latC);
1734
        }
1735
1736
        $t = tan(M_PI / 4 - $latitude / 2) / ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2);
1737
        $Q = $H / $t ** $B;
1738
        $S = ($Q - 1 / $Q) / 2;
1739
        $T = ($Q + 1 / $Q) / 2;
1740
        $V = sin($B * ($longitude - $lonO));
1741
        $U = (-$V * cos($gammaO) + $S * sin($gammaO)) / $T;
1742
        $v = $A * log((1 - $U) / (1 + $U)) / (2 * $B);
1743
        $u = ($A * atan2($S * cos($gammaO) + $V * sin($gammaO), cos($B * ($longitude - $lonO))) / $B) - (abs($uC) * static::sign($latC));
1744
1745
        $easting = $v * cos($gammaC) + $u * sin($gammaC) + $eastingAtProjectionCentre->asMetres()->getValue();
1746
        $northing = $u * cos($gammaC) - $v * sin($gammaC) + $northingAtProjectionCentre->asMetres()->getValue();
1747
1748
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1749
    }
1750
1751
    /**
1752
     * Laborde Oblique Mercator.
1753
     */
1754
    public function obliqueMercatorLaborde(
1755
        Projected $to,
1756
        Angle $latitudeOfProjectionCentre,
1757
        Angle $longitudeOfProjectionCentre,
1758
        Angle $azimuthAtProjectionCentre,
1759
        Scale $scaleFactorAtProjectionCentre,
1760
        Length $falseEasting,
1761
        Length $falseNorthing
1762
    ): ProjectedPoint {
1763
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1764
        $latitude = $this->latitude->asRadians()->getValue();
1765
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1766
        $alphaC = $azimuthAtProjectionCentre->asRadians()->getValue();
1767
        $kC = $scaleFactorAtProjectionCentre->asUnity()->getValue();
1768
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1769
        $e = $ellipsoid->getEccentricity();
1770
        $e2 = $ellipsoid->getEccentricitySquared();
1771
1772
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1773
        $latS = self::asin(sin($latC) / $B);
1774
        $R = $a * $kC * (sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2));
1775
        $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));
1776
1777
        $L = $B * $this->normaliseLongitude($this->longitude->subtract($longitudeOfProjectionCentre))->asRadians()->getValue();
1778
        $q = $C + $B * log(tan(M_PI / 4 + $latitude / 2) * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1779
        $P = 2 * atan(M_E ** $q) - M_PI / 2;
1780
        $U = cos($P) * cos($L) * cos($latS) + sin($P) * sin($latS);
1781
        $V = cos($P) * cos($L) * sin($latS) - sin($P) * cos($latS);
1782
        $W = cos($P) * sin($L);
1783
        $d = hypot($U, $V);
1784
        if ($d === 0.0) {
1785
            $LPrime = 0;
1786
            $PPrime = static::sign($W) * M_PI / 2;
1787
        } else {
1788
            $LPrime = 2 * atan($V / ($U + $d));
1789
            $PPrime = atan($W / $d);
1790
        }
1791
        $H = new ComplexNumber(-$LPrime, log(tan(M_PI / 4 + $PPrime / 2)));
1792
        $G = (new ComplexNumber(1 - cos(2 * $alphaC), sin(2 * $alphaC)))->divide(new ComplexNumber(12, 0));
1793
1794
        $easting = $falseEasting->asMetres()->getValue() + $R * $H->pow(3)->multiply($G)->add($H)->getImaginary();
1795
        $northing = $falseNorthing->asMetres()->getValue() + $R * $H->pow(3)->multiply($G)->add($H)->getReal();
1796
1797
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1798
    }
1799
1800
    /**
1801
     * Transverse Mercator.
1802
     */
1803
    public function transverseMercator(
1804
        Projected $to,
1805
        Angle $latitudeOfNaturalOrigin,
1806
        Angle $longitudeOfNaturalOrigin,
1807
        Scale $scaleFactorAtNaturalOrigin,
1808
        Length $falseEasting,
1809
        Length $falseNorthing
1810
    ): ProjectedPoint {
1811
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1812
        $latitude = $this->latitude->asRadians()->getValue();
1813
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1814
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1815
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1816
        $e = $ellipsoid->getEccentricity();
1817
        $f = $ellipsoid->getFlattening();
1818
1819
        $n = $f / (2 - $f);
1820
        $B = ($a / (1 + $n)) * (1 + $n ** 2 / 4 + $n ** 4 / 64 + $n ** 6 / 256 + (25 / 16384) * $n ** 8);
1821
1822
        $h1 = $n / 2 - (2 / 3) * $n ** 2 + (5 / 16) * $n ** 3 + (41 / 180) * $n ** 4 - (127 / 288) * $n ** 5 + (7891 / 37800) * $n ** 6 + (72161 / 387072) * $n ** 7 - (18975107 / 50803200) * $n ** 8;
1823
        $h2 = (13 / 48) * $n ** 2 - (3 / 5) * $n ** 3 + (557 / 1440) * $n ** 4 + (281 / 630) * $n ** 5 - (1983433 / 1935360) * $n ** 6 + (13769 / 28800) * $n ** 7 + (148003883 / 174182400) * $n ** 8;
1824
        $h3 = (61 / 240) * $n ** 3 - (103 / 140) * $n ** 4 + (15061 / 26880) * $n ** 5 + (167603 / 181440) * $n ** 6 - (67102379 / 29030400) * $n ** 7 + (79682431 / 79833600) * $n ** 8;
1825
        $h4 = (49561 / 161280) * $n ** 4 - (179 / 168) * $n ** 5 + (6601661 / 7257600) * $n ** 6 + (97445 / 49896) * $n ** 7 - (40176129013 / 7664025600) * $n ** 8;
1826
        $h5 = (34729 / 80640) * $n ** 5 - (3418889 / 1995840) * $n ** 6 + (14644087 / 9123840) * $n ** 7 + (2605413599 / 622702080) * $n ** 8;
1827
        $h6 = (212378941 / 319334400) * $n ** 6 - (30705481 / 10378368) * $n ** 7 + (175214326799 / 58118860800) * $n ** 8;
1828
        $h7 = (1522256789 / 1383782400) * $n ** 7 - (16759934899 / 3113510400) * $n ** 8;
1829
        $h8 = (1424729850961 / 743921418240) * $n ** 8;
1830
1831
        if ($latitudeOrigin === 0.0) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === 0.0 is always false.
Loading history...
1832
            $mO = 0;
1833
        } elseif ($latitudeOrigin === M_PI / 2) {
1834
            $mO = $B * M_PI / 2;
1835
        } elseif ($latitudeOrigin === -M_PI / 2) {
1836
            $mO = $B * -M_PI / 2;
1837
        } else {
1838
            $qO = asinh(tan($latitudeOrigin)) - ($e * atanh($e * sin($latitudeOrigin)));
1839
            $betaO = atan(sinh($qO));
1840
            $xiO0 = self::asin(sin($betaO));
1841
            $xiO1 = $h1 * sin(2 * $xiO0);
1842
            $xiO2 = $h2 * sin(4 * $xiO0);
1843
            $xiO3 = $h3 * sin(6 * $xiO0);
1844
            $xiO4 = $h4 * sin(8 * $xiO0);
1845
            $xiO5 = $h5 * sin(10 * $xiO0);
1846
            $xiO6 = $h6 * sin(12 * $xiO0);
1847
            $xiO7 = $h7 * sin(14 * $xiO0);
1848
            $xiO8 = $h8 * sin(16 * $xiO0);
1849
            $xiO = $xiO0 + $xiO1 + $xiO2 + $xiO3 + $xiO4 + $xiO5 + $xiO6 + $xiO7 + $xiO8;
1850
            $mO = $B * $xiO;
1851
        }
1852
1853
        $Q = asinh(tan($latitude)) - ($e * atanh($e * sin($latitude)));
1854
        $beta = atan(sinh($Q));
1855
        $eta0 = atanh(cos($beta) * sin($this->normaliseLongitude($this->longitude->subtract($longitudeOfNaturalOrigin))->asRadians()->getValue()));
1856
        $xi0 = self::asin(sin($beta) * cosh($eta0));
1857
        $xi1 = $h1 * sin(2 * $xi0) * cosh(2 * $eta0);
1858
        $eta1 = $h1 * cos(2 * $xi0) * sinh(2 * $eta0);
1859
        $xi2 = $h2 * sin(4 * $xi0) * cosh(4 * $eta0);
1860
        $eta2 = $h2 * cos(4 * $xi0) * sinh(4 * $eta0);
1861
        $xi3 = $h3 * sin(6 * $xi0) * cosh(6 * $eta0);
1862
        $eta3 = $h3 * cos(6 * $xi0) * sinh(6 * $eta0);
1863
        $xi4 = $h4 * sin(8 * $xi0) * cosh(8 * $eta0);
1864
        $eta4 = $h4 * cos(8 * $xi0) * sinh(8 * $eta0);
1865
        $xi5 = $h5 * sin(10 * $xi0) * cosh(10 * $eta0);
1866
        $eta5 = $h5 * cos(10 * $xi0) * sinh(10 * $eta0);
1867
        $xi6 = $h6 * sin(12 * $xi0) * cosh(12 * $eta0);
1868
        $eta6 = $h6 * cos(12 * $xi0) * sinh(12 * $eta0);
1869
        $xi7 = $h7 * sin(14 * $xi0) * cosh(14 * $eta0);
1870
        $eta7 = $h7 * cos(14 * $xi0) * sinh(14 * $eta0);
1871
        $xi8 = $h8 * sin(16 * $xi0) * cosh(16 * $eta0);
1872
        $eta8 = $h8 * cos(16 * $xi0) * sinh(16 * $eta0);
1873
        $xi = $xi0 + $xi1 + $xi2 + $xi3 + $xi4 + $xi5 + $xi6 + $xi7 + $xi8;
1874
        $eta = $eta0 + $eta1 + $eta2 + $eta3 + $eta4 + $eta5 + $eta6 + $eta7 + $eta8;
1875
1876
        $easting = $falseEasting->asMetres()->getValue() + $kO * $B * $eta;
1877
        $northing = $falseNorthing->asMetres()->getValue() + $kO * ($B * $xi - $mO);
1878
1879
        $height = count($to->getCoordinateSystem()->getAxes()) === 3 ? $this->height : null;
1880
1881
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch, $height);
1882
    }
1883
1884
    /**
1885
     * Transverse Mercator Zoned Grid System
1886
     * If locations fall outwith the fixed zones the general Transverse Mercator method (code 9807) must be used for
1887
     * each zone.
1888
     */
1889
    public function transverseMercatorZonedGrid(
1890
        Projected $to,
1891
        Angle $latitudeOfNaturalOrigin,
1892
        Angle $initialLongitude,
1893
        Angle $zoneWidth,
1894
        Scale $scaleFactorAtNaturalOrigin,
1895
        Length $falseEasting,
1896
        Length $falseNorthing
1897
    ): ProjectedPoint {
1898
        $W = $zoneWidth->asDegrees()->getValue();
1899
        $Z = (int) ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / $W) % (int) (360 / $W) + 1;
1900
1901
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * $W - $W / 2));
1902
        $falseEasting = $falseEasting->add(new Metre($Z * 1000000));
1903
1904
        return $this->transverseMercator($to, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
1905
    }
1906
1907
    /**
1908
     * New Zealand Map Grid.
1909
     */
1910
    public function newZealandMapGrid(
1911
        Projected $to,
1912
        Angle $latitudeOfNaturalOrigin,
1913
        Angle $longitudeOfNaturalOrigin,
1914
        Length $falseEasting,
1915
        Length $falseNorthing
1916
    ): ProjectedPoint {
1917
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1918
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1919
1920
        $deltaLatitudeToOrigin = Angle::convert($this->latitude->subtract($latitudeOfNaturalOrigin), Angle::EPSG_ARC_SECOND)->getValue();
1921
        $deltaLongitudeToOrigin = $this->longitude->subtract($longitudeOfNaturalOrigin)->asRadians();
1922
1923
        $deltaPsi = 0;
1924
        $deltaPsi += 0.6399175073 * ($deltaLatitudeToOrigin * 0.00001) ** 1;
1925
        $deltaPsi += -0.1358797613 * ($deltaLatitudeToOrigin * 0.00001) ** 2;
1926
        $deltaPsi += 0.063294409 * ($deltaLatitudeToOrigin * 0.00001) ** 3;
1927
        $deltaPsi += -0.02526853 * ($deltaLatitudeToOrigin * 0.00001) ** 4;
1928
        $deltaPsi += 0.0117879 * ($deltaLatitudeToOrigin * 0.00001) ** 5;
1929
        $deltaPsi += -0.0055161 * ($deltaLatitudeToOrigin * 0.00001) ** 6;
1930
        $deltaPsi += 0.0026906 * ($deltaLatitudeToOrigin * 0.00001) ** 7;
1931
        $deltaPsi += -0.001333 * ($deltaLatitudeToOrigin * 0.00001) ** 8;
1932
        $deltaPsi += 0.00067 * ($deltaLatitudeToOrigin * 0.00001) ** 9;
1933
        $deltaPsi += -0.00034 * ($deltaLatitudeToOrigin * 0.00001) ** 10;
1934
1935
        $zeta = new ComplexNumber($deltaPsi, $deltaLongitudeToOrigin->getValue());
1936
1937
        $B1 = new ComplexNumber(0.7557853228, 0.0);
1938
        $B2 = new ComplexNumber(0.249204646, 0.003371507);
1939
        $B3 = new ComplexNumber(-0.001541739, 0.041058560);
1940
        $B4 = new ComplexNumber(-0.10162907, 0.01727609);
1941
        $B5 = new ComplexNumber(-0.26623489, -0.36249218);
1942
        $B6 = new ComplexNumber(-0.6870983, -1.1651967);
1943
        $z = new ComplexNumber(0, 0);
1944
        $z = $z->add($B1->multiply($zeta->pow(1)));
1945
        $z = $z->add($B2->multiply($zeta->pow(2)));
1946
        $z = $z->add($B3->multiply($zeta->pow(3)));
1947
        $z = $z->add($B4->multiply($zeta->pow(4)));
1948
        $z = $z->add($B5->multiply($zeta->pow(5)));
1949
        $z = $z->add($B6->multiply($zeta->pow(6)));
1950
1951
        $easting = $falseEasting->asMetres()->getValue() + $z->getImaginary() * $a;
1952
        $northing = $falseNorthing->asMetres()->getValue() + $z->getReal() * $a;
1953
1954
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1955
    }
1956
1957
    /**
1958
     * Madrid to ED50 polynomial.
1959
     */
1960
    public function madridToED50Polynomial(
1961
        Geographic2D $to,
1962
        Scale $A0,
1963
        Scale $A1,
1964
        Scale $A2,
1965
        Scale $A3,
1966
        Angle $B00,
1967
        Scale $B0,
1968
        Scale $B1,
1969
        Scale $B2,
1970
        Scale $B3
1971
    ): self {
1972
        $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());
1973
        $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()));
1974
1975
        return self::create($to, $this->latitude->add($dLatitude), $this->longitude->add($dLongitude), null, $this->epoch);
1976
    }
1977
1978
    /**
1979
     * Geographic3D to 2D conversion.
1980
     */
1981
    public function threeDToTwoD(
1982
        Geographic2D|Geographic3D $to
1983
    ): self {
1984
        if ($to instanceof Geographic2D) {
1985
            return static::create($to, $this->latitude, $this->longitude, null, $this->epoch);
1986
        }
1987
1988
        return static::create($to, $this->latitude, $this->longitude, new Metre(0), $this->epoch);
1989
    }
1990
1991
    /**
1992
     * Geographic2D offsets.
1993
     * This transformation allows calculation of coordinates in the target system by adding the parameter value to the
1994
     * coordinate values of the point in the source system.
1995
     */
1996
    public function geographic2DOffsets(
1997
        Geographic2D|Geographic3D $to,
1998
        Angle $latitudeOffset,
1999
        Angle $longitudeOffset
2000
    ): self {
2001
        $toLatitude = $this->latitude->add($latitudeOffset);
2002
        $toLongitude = $this->longitude->add($longitudeOffset);
2003
2004
        return static::create($to, $toLatitude, $toLongitude, null, $this->epoch);
2005
    }
2006
2007
    /*
2008
     * Geographic2D with Height Offsets.
2009
     * This transformation allows calculation of coordinates in the target system by adding the parameter value to the
2010
     * coordinate values of the point in the source system.
2011
     */
2012
    public function geographic2DWithHeightOffsets(
2013
        Compound $to,
2014
        Angle $latitudeOffset,
2015
        Angle $longitudeOffset,
2016
        Length $geoidUndulation
2017
    ): CompoundPoint {
2018
        assert($this->height instanceof Length);
2019
        $toLatitude = $this->latitude->add($latitudeOffset);
2020
        $toLongitude = $this->longitude->add($longitudeOffset);
2021
        $toHeight = $this->height->add($geoidUndulation);
2022
2023
        assert($to->getHorizontal() instanceof Geographic2D);
2024
        $horizontal = static::create($to->getHorizontal(), $toLatitude, $toLongitude, null, $this->epoch);
2025
        $vertical = VerticalPoint::create($to->getVertical(), $toHeight, $this->epoch);
2026
2027
        return CompoundPoint::create($to, $horizontal, $vertical, $this->epoch);
2028
    }
2029
2030
    /**
2031
     * General polynomial.
2032
     * @param Coefficient[] $powerCoefficients
2033
     */
2034
    public function generalPolynomial(
2035
        Geographic2D|Geographic3D $to,
2036
        Angle $ordinate1OfEvaluationPointInSourceCRS,
2037
        Angle $ordinate2OfEvaluationPointInSourceCRS,
2038
        Angle $ordinate1OfEvaluationPointInTargetCRS,
2039
        Angle $ordinate2OfEvaluationPointInTargetCRS,
2040
        Scale $scalingFactorForSourceCRSCoordDifferences,
2041
        Scale $scalingFactorForTargetCRSCoordDifferences,
2042
        Scale $A0,
2043
        Scale $B0,
2044
        array $powerCoefficients,
2045
        bool $inReverse
2046
    ): self {
2047
        $xs = $this->latitude->getValue();
2048
        $ys = $this->longitude->getValue();
2049
2050
        $t = $this->generalPolynomialUnitless(
2051
            $xs,
2052
            $ys,
2053
            $ordinate1OfEvaluationPointInSourceCRS,
2054
            $ordinate2OfEvaluationPointInSourceCRS,
2055
            $ordinate1OfEvaluationPointInTargetCRS,
2056
            $ordinate2OfEvaluationPointInTargetCRS,
2057
            $scalingFactorForSourceCRSCoordDifferences,
2058
            $scalingFactorForTargetCRSCoordDifferences,
2059
            $A0,
2060
            $B0,
2061
            $powerCoefficients,
2062
            $inReverse
2063
        );
2064
2065
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2066
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2067
2068
        return static::create(
2069
            $to,
2070
            Angle::makeUnit($t['xt'], $xtUnit),
2071
            Angle::makeUnit($t['yt'], $ytUnit),
2072
            $this->height,
2073
            $this->epoch
2074
        );
2075
    }
2076
2077
    /**
2078
     * Reversible polynomial.
2079
     * @param Coefficient[] $powerCoefficients
2080
     */
2081
    public function reversiblePolynomial(
2082
        Geographic2D|Geographic3D $to,
2083
        Angle $ordinate1OfEvaluationPoint,
2084
        Angle $ordinate2OfEvaluationPoint,
2085
        Scale $scalingFactorForCoordDifferences,
2086
        Scale $A0,
2087
        Scale $B0,
2088
        $powerCoefficients
2089
    ): self {
2090
        $xs = $this->latitude->getValue();
2091
        $ys = $this->longitude->getValue();
2092
2093
        $t = $this->reversiblePolynomialUnitless(
2094
            $xs,
2095
            $ys,
2096
            $ordinate1OfEvaluationPoint,
2097
            $ordinate2OfEvaluationPoint,
2098
            $scalingFactorForCoordDifferences,
2099
            $A0,
2100
            $B0,
2101
            $powerCoefficients
2102
        );
2103
2104
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2105
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2106
2107
        return static::create(
2108
            $to,
2109
            Angle::makeUnit($t['xt'], $xtUnit),
2110
            Angle::makeUnit($t['yt'], $ytUnit),
2111
            $this->height,
2112
            $this->epoch
2113
        );
2114
    }
2115
2116
    /**
2117
     * Axis Order Reversal.
2118
     */
2119
    public function axisReversal(
2120
        Geographic2D|Geographic3D $to
2121
    ): self {
2122
        // axes are read in from the CRS, this is a book-keeping adjustment only
2123
        return static::create($to, $this->latitude, $this->longitude, $this->height, $this->epoch);
2124
    }
2125
2126
    /**
2127
     * Ordnance Survey National Transformation
2128
     * Geodetic transformation between ETRS89 (or WGS 84) and OSGB36 / National Grid.  Uses ETRS89 / National Grid as
2129
     * an intermediate coordinate system for bi-linear interpolation of gridded grid coordinate differences.
2130
     */
2131
    public function OSTN15(
2132
        Projected $to,
0 ignored issues
show
Unused Code introduced by
The parameter $to 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

2132
        /** @scrutinizer ignore-unused */ Projected $to,

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...
2133
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2134
    ): ProjectedPoint {
2135
        $osgb36NationalGrid = Projected::fromSRID(Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID);
2136
        $etrs89NationalGrid = new Projected(
2137
            'ETRS89 / National Grid',
2138
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2139
            Datum::fromSRID(Datum::EPSG_EUROPEAN_TERRESTRIAL_REFERENCE_SYSTEM_1989_ENSEMBLE),
2140
            $osgb36NationalGrid->getBoundingArea()
2141
        );
2142
2143
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2144
2145
        return $eastingAndNorthingDifferenceFile->applyForwardHorizontalAdjustment($projected);
2146
    }
2147
2148
    /**
2149
     * Geog3D to Geog2D+GravityRelatedHeight (OSGM-GB).
2150
     * Uses ETRS89 / National Grid as an intermediate coordinate system for bi-linear interpolation of gridded grid
2151
     * coordinate differences.
2152
     */
2153
    public function geographic3DTo2DPlusGravityHeightOSGM15(
2154
        Compound $to,
2155
        OSTNOSGM15Grid $geoidHeightCorrectionModelFile
2156
    ): CompoundPoint {
2157
        assert($this->height instanceof Length);
2158
        $osgb36NationalGrid = Projected::fromSRID(Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID);
2159
        $etrs89NationalGrid = new Projected(
2160
            'ETRS89 / National Grid',
2161
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2162
            Datum::fromSRID(Datum::EPSG_EUROPEAN_TERRESTRIAL_REFERENCE_SYSTEM_1989_ENSEMBLE),
2163
            $osgb36NationalGrid->getBoundingArea()
2164
        );
2165
2166
        /** @var ProjectedPoint $projected */
2167
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2168
2169
        assert($to->getHorizontal() instanceof Geographic2D);
2170
        $horizontalPoint = self::create(
2171
            $to->getHorizontal(),
2172
            $this->latitude,
2173
            $this->longitude,
2174
            null,
2175
            $this->getCoordinateEpoch()
2176
        );
2177
2178
        $verticalPoint = VerticalPoint::create(
2179
            $to->getVertical(),
2180
            $this->height->subtract($geoidHeightCorrectionModelFile->getHeightAdjustment($projected)),
2181
            $this->getCoordinateEpoch()
2182
        );
2183
2184
        return CompoundPoint::create(
2185
            $to,
2186
            $horizontalPoint,
2187
            $verticalPoint,
2188
            $this->getCoordinateEpoch()
2189
        );
2190
    }
2191
2192
    /**
2193
     * Geographic3D to GravityRelatedHeight (OSGM-GB).
2194
     * Uses ETRS89 / National Grid as an intermediate coordinate system for bi-linear interpolation of gridded grid
2195
     * coordinate differences.
2196
     */
2197
    public function geographic3DToGravityHeightOSGM15(
2198
        Vertical $to,
2199
        OSTNOSGM15Grid $geoidHeightCorrectionModelFile
2200
    ): VerticalPoint {
2201
        assert($this->height instanceof Length);
2202
        $osgb36NationalGrid = Projected::fromSRID(Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID);
2203
        $etrs89NationalGrid = new Projected(
2204
            'ETRS89 / National Grid',
2205
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2206
            Datum::fromSRID(Datum::EPSG_EUROPEAN_TERRESTRIAL_REFERENCE_SYSTEM_1989_ENSEMBLE),
2207
            $osgb36NationalGrid->getBoundingArea()
2208
        );
2209
2210
        $projected = $this->transverseMercator($etrs89NationalGrid, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2211
2212
        return VerticalPoint::create(
2213
            $to,
2214
            $this->height->subtract($geoidHeightCorrectionModelFile->getHeightAdjustment($projected)),
2215
            $this->getCoordinateEpoch()
2216
        );
2217
    }
2218
2219
    /**
2220
     * Geog3D to Geog2D+GravityRelatedHeight.
2221
     */
2222
    public function geographic3DTo2DPlusGravityHeightFromGrid(
2223
        Compound $to,
2224
        GeographicGeoidHeightGrid $geoidHeightCorrectionModelFile
2225
    ): CompoundPoint {
2226
        assert($this->height instanceof Length);
2227
        assert($to->getHorizontal() instanceof Geographic);
2228
        $horizontalPoint = self::create(
2229
            $to->getHorizontal(),
2230
            $this->latitude,
2231
            $this->longitude,
2232
            null,
2233
            $this->getCoordinateEpoch()
2234
        );
2235
2236
        $verticalPoint = VerticalPoint::create(
2237
            $to->getVertical(),
2238
            $this->height->subtract($geoidHeightCorrectionModelFile->getHeightAdjustment($this)),
2239
            $this->getCoordinateEpoch()
2240
        );
2241
2242
        return CompoundPoint::create(
2243
            $to,
2244
            $horizontalPoint,
2245
            $verticalPoint,
2246
            $this->getCoordinateEpoch()
2247
        );
2248
    }
2249
2250
    /**
2251
     * Geographic3D to GravityRelatedHeight.
2252
     */
2253
    public function geographic3DToGravityHeightFromGrid(
2254
        Vertical $to,
2255
        GeographicGeoidHeightGrid $geoidHeightCorrectionModelFile
2256
    ): VerticalPoint {
2257
        assert($this->height instanceof Length);
2258
2259
        return VerticalPoint::create(
2260
            $to,
2261
            $this->height->subtract($geoidHeightCorrectionModelFile->getHeightAdjustment($this)),
2262
            $this->getCoordinateEpoch()
2263
        );
2264
    }
2265
2266
    /**
2267
     * NADCON5.
2268
     * @internal just a wrapper
2269
     */
2270
    public function offsetsFromGridNADCON5(
2271
        Geographic2D|Geographic3D $to,
2272
        NADCON5Grid $latitudeDifferenceFile,
2273
        NADCON5Grid $longitudeDifferenceFile,
2274
        ?NADCON5Grid $ellipsoidalHeightDifferenceFile,
2275
        bool $inReverse
2276
    ): self {
2277
        $aggregation = new NADCON5Grids($longitudeDifferenceFile, $latitudeDifferenceFile, $ellipsoidalHeightDifferenceFile);
2278
2279
        return $this->offsetsFromGrid($to, $aggregation, $inReverse);
2280
    }
2281
2282
    /**
2283
     * Geographic offsets from grid.
2284
     */
2285
    public function offsetsFromGrid(
2286
        Geographic2D|Geographic3D $to,
2287
        GeographicGrid $offsetsFile,
2288
        bool $inReverse
2289
    ): self {
2290
        if (!$inReverse) {
2291
            return $offsetsFile->applyForwardAdjustment($this, $to);
2292
        }
2293
2294
        return $offsetsFile->applyReverseAdjustment($this, $to);
2295
    }
2296
2297
    public function localOrthographic(
2298
        Projected $to,
2299
        Angle $latitudeOfProjectionCentre,
2300
        Angle $longitudeOfProjectionCentre,
2301
        Angle $azimuthAtProjectionCentre,
2302
        Scale $scaleFactorAtProjectionCentre,
2303
        Length $eastingAtProjectionCentre,
2304
        Length $northingAtProjectionCentre
2305
    ): ProjectedPoint {
2306
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
2307
        $latitude = $this->latitude->asRadians()->getValue();
2308
        $longitude = $this->longitude->asRadians()->getValue();
2309
        $latitudeCentre = $latitudeOfProjectionCentre->asRadians()->getValue();
2310
        $longitudeCentre = $longitudeOfProjectionCentre->asRadians()->getValue();
2311
        $azimuthCentre = $azimuthAtProjectionCentre->asRadians()->getValue();
2312
        $scaleFactorCentre = $scaleFactorAtProjectionCentre->asUnity()->getValue();
2313
2314
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
2315
        $e2 = $ellipsoid->getEccentricitySquared();
2316
        $v = $a / sqrt(1 - $e2 * sin($latitude) ** 2);
2317
        $vc = $a / sqrt(1 - $e2 * sin($latitudeCentre) ** 2);
2318
2319
        $xp = $v * cos($latitude) * sin($longitude - $longitudeCentre);
2320
        $yp = -sin($latitudeCentre) * ($v * cos($latitude) * cos($longitude - $longitudeCentre) - $vc * cos($latitudeCentre)) + cos($latitudeCentre) * ($v * (1 - $e2) * sin($latitude) - $vc * (1 - $e2) * sin($latitudeCentre));
2321
2322
        $easting = $eastingAtProjectionCentre->asMetres()->getValue() + $scaleFactorCentre * (cos($azimuthCentre) * $xp - sin($azimuthCentre) * $yp);
2323
        $northing = $northingAtProjectionCentre->asMetres()->getValue() + $scaleFactorCentre * (sin($azimuthCentre) * $xp + cos($azimuthCentre) * $yp);
2324
2325
        return ProjectedPoint::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
2326
    }
2327
2328
    public function asGeographicValue(): GeographicValue
2329
    {
2330
        return new GeographicValue($this->latitude, $this->longitude, $this->height, $this->crs->getDatum());
2331
    }
2332
2333
    public function asUTMPoint(): UTMPoint
2334
    {
2335
        $hemisphere = $this->getLatitude()->asDegrees()->getValue() >= 0 ? UTMPoint::HEMISPHERE_NORTH : UTMPoint::HEMISPHERE_SOUTH;
2336
2337
        $initialLongitude = new Degree(-180);
2338
        $zone = (int) ($this->longitude->subtract($initialLongitude)->asDegrees()->getValue() / 6) % (360 / 6) + 1;
2339
2340
        if ($hemisphere === UTMPoint::HEMISPHERE_NORTH) {
2341
            $derivingConversion = 'urn:ogc:def:coordinateOperation:EPSG::' . ($zone + 16000);
2342
        } else {
2343
            $derivingConversion = 'urn:ogc:def:coordinateOperation:EPSG::' . ($zone + 16100);
2344
        }
2345
2346
        $srid = 'urn:ogc:def:crs,' . str_replace('urn:ogc:def:', '', $this->crs->getSRID()) . ',' . str_replace('urn:ogc:def:', '', Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M) . ',' . str_replace('urn:ogc:def:', '', $derivingConversion);
2347
2348
        $projectedCRS = new Projected(
2349
            $srid,
2350
            Cartesian::fromSRID(Cartesian::EPSG_2D_AXES_EASTING_NORTHING_E_N_ORIENTATIONS_EAST_NORTH_UOM_M),
2351
            $this->crs->getDatum(),
2352
            BoundingArea::createWorld() // this is a dummy CRS for the transform only, details don't matter (UTMPoint creates own)
2353
        );
2354
2355
        /** @var ProjectedPoint $asProjected */
2356
        $asProjected = $this->performOperation($derivingConversion, $projectedCRS, false);
2357
2358
        return new UTMPoint($this->crs, $asProjected->getEasting(), $asProjected->getNorthing(), $zone, $hemisphere, $this->epoch);
2359
    }
2360
}
2361