Completed
Push — master ( 1d678e...613697 )
by Doug
02:45
created

LatLng::transformDatum()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 8
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 10
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
declare(strict_types=1);
4
5
namespace PHPCoord;
6
7
/**
8
 * Latitude/Longitude reference.
9
 *
10
 * @author Jonathan Stott
11
 * @author Doug Wright
12
 */
13
class LatLng
14
{
15
    /**
16
     * Latitude.
17
     *
18
     * @var float
19
     */
20
    protected $lat;
21
22
    /**
23
     * Longitude.
24
     *
25
     * @var float
26
     */
27
    protected $lng;
28
29
    /**
30
     * Height.
31
     *
32
     * @var float
33
     */
34
    protected $h;
35
36
    /**
37
     * Reference ellipsoid the co-ordinates are from.
38
     *
39
     * @var RefEll
40
     */
41
    protected $refEll;
42
43
    /**
44
     * Create a new LatLng object from the given latitude and longitude.
45
     *
46
     * @param float  $lat
47
     * @param float  $lng
48
     * @param int    $height
49
     * @param RefEll $refEll
50
     */
51 36
    public function __construct(float $lat, float $lng, int $height, RefEll $refEll)
52
    {
53 36
        $this->lat = $lat;
54 36
        $this->lng = $lng;
55 36
        $this->h = $height;
56 36
        $this->refEll = $refEll;
57 36
    }
58
59
    /**
60
     * Return a string representation of this LatLng object.
61
     *
62
     * @return string
63
     */
64 13
    public function __toString(): string
65
    {
66 13
        return "({$this->getLat()}, {$this->getLng()})";
67
    }
68
69
    /**
70
     * @return float
71
     */
72 29
    public function getLat(): float
73
    {
74 29
        return round($this->lat, 5);
75
    }
76
77
    /**
78
     * @return float
79
     */
80 29
    public function getLng(): float
81
    {
82 29
        return round($this->lng, 5);
83
    }
84
85
    /**
86
     * @return int
87
     */
88 15
    public function getH(): int
89
    {
90 15
        return $this->h;
91
    }
92
93
    /**
94
     * @return RefEll
95
     */
96 9
    public function getRefEll(): RefEll
97
    {
98 9
        return $this->refEll;
99
    }
100
101
    /**
102
     * Calculate the surface distance between this LatLng object and the one
103
     * passed in as a parameter.
104
     *
105
     * @param LatLng $to a LatLng object to measure the surface distance to
106
     *
107
     * @return int distance in metres
108
     */
109 2
    public function distance(self $to): int
110
    {
111 2
        if ($this->refEll != $to->refEll) {
112 1
            throw new \RuntimeException('Source and destination co-ordinates are not using the same ellipsoid');
113
        }
114
115
        //Mean radius definition taken from Wikipedia
116 1
        $er = ((2 * $this->refEll->getMaj()) + $this->refEll->getMin()) / 3;
117
118 1
        $latFrom = deg2rad($this->lat);
119 1
        $latTo = deg2rad($to->lat);
120 1
        $lngFrom = deg2rad($this->lng);
121 1
        $lngTo = deg2rad($to->lng);
122
123 1
        $d = acos(sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lngTo - $lngFrom)) * $er;
124
125 1
        return (int) round($d);
126
    }
127
128
    /**
129
     * Convert this LatLng object to OSGB36 datum.
130
     * Reference values for transformation are taken from OS document
131
     * "A Guide to Coordinate Systems in Great Britain".
132
     *
133
     * @return self
134
     */
135 4
    public function toOSGB36(): self
136
    {
137 4
        if ($this->refEll == RefEll::airy1830()) {
138 2
            return $this;
139
        }
140
141 2
        $asWGS84 = $this->toWGS84();
142
143 1
        $tx = -446.448;
144 1
        $ty = 125.157;
145 1
        $tz = -542.060;
146 1
        $s = 0.0000204894;
147 1
        $rx = deg2rad(-0.1502 / 3600);
148 1
        $ry = deg2rad(-0.2470 / 3600);
149 1
        $rz = deg2rad(-0.8421 / 3600);
150
151 1
        return $asWGS84->transformDatum(RefEll::airy1830(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
152
    }
153
154
    /**
155
     * Convert this LatLng object to ED50 datum.
156
     * Reference values for transformation are taken from http://www.globalmapper.com/helpv9/datum_list.htm .
157
     *
158
     * @return self
159
     */
160 1
    public function toED50(): self
161
    {
162 1
        if ($this->refEll == RefEll::international1924()) {
163
            return $this;
164
        }
165
166 1
        $asWGS84 = $this->toWGS84();
167
168 1
        $tx = 87;
169 1
        $ty = 98;
170 1
        $tz = 121;
171 1
        $s = 0;
172 1
        $rx = deg2rad(0);
173 1
        $ry = deg2rad(0);
174 1
        $rz = deg2rad(0);
175
176 1
        return $asWGS84->transformDatum(RefEll::international1924(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
177
    }
178
179
    /**
180
     * Convert this LatLng object to NAD27 datum.
181
     * Reference values for transformation are taken from Wikipedia.
182
     *
183
     * @return self
184
     */
185 1
    public function toNAD27(): self
186
    {
187 1
        if ($this->refEll == RefEll::clarke1866()) {
188
            return $this;
189
        }
190
191 1
        $asWGS84 = $this->toWGS84();
192
193 1
        $tx = 8;
194 1
        $ty = -160;
195 1
        $tz = -176;
196 1
        $s = 0;
197 1
        $rx = deg2rad(0);
198 1
        $ry = deg2rad(0);
199 1
        $rz = deg2rad(0);
200
201 1
        return $asWGS84->transformDatum(RefEll::clarke1866(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
202
    }
203
204
    /**
205
     * Convert this LatLng object to Ireland 1975 datum.
206
     * Reference values for transformation are taken from OSI document
207
     * "Making maps compatible with GPS".
208
     *
209
     * @return self
210
     */
211 1
    public function toIreland1975(): self
212
    {
213 1
        if ($this->refEll == RefEll::airyModified()) {
214
            return $this;
215
        }
216
217 1
        $asWGS84 = $this->toWGS84();
218
219 1
        $tx = -482.530;
220 1
        $ty = 130.596;
221 1
        $tz = -564.557;
222 1
        $s = -0.00000815;
223 1
        $rx = deg2rad(-1.042 / 3600);
224 1
        $ry = deg2rad(-0.214 / 3600);
225 1
        $rz = deg2rad(-0.631 / 3600);
226
227 1
        return $asWGS84->transformDatum(RefEll::airyModified(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
228
    }
229
230
    /**
231
     * Convert this LatLng object to WGS84 datum.
232
     *
233
     * @return self
234
     */
235 20
    public function toWGS84(): self
236
    {
237 20
        switch ($this->refEll) {
238 20
            case RefEll::wgs84():
239 13
                return $this; //do nothing
240
241 7
            case RefEll::airy1830(): // values from OSGB document "A Guide to Coordinate Systems in Great Britain"
242 2
                $tx = 446.448;
243 2
                $ty = -125.157;
244 2
                $tz = 542.060;
245 2
                $s = -0.0000204894;
246 2
                $rx = deg2rad(0.1502 / 3600);
247 2
                $ry = deg2rad(0.2470 / 3600);
248 2
                $rz = deg2rad(0.8421 / 3600);
249 2
                break;
250
251 5
            case RefEll::airyModified(): // values from OSI document "Making maps compatible with GPS"
252 1
                $tx = 482.530;
253 1
                $ty = -130.596;
254 1
                $tz = 564.557;
255 1
                $s = 0.00000815;
256 1
                $rx = deg2rad(1.042 / 3600);
257 1
                $ry = deg2rad(0.214 / 3600);
258 1
                $rz = deg2rad(0.631 / 3600);
259 1
                break;
260
261 4
            case RefEll::clarke1866(): // assumes NAD27, values from Wikipedia
262 1
                $tx = -8;
263 1
                $ty = 160;
264 1
                $tz = 176;
265 1
                $s = 0;
266 1
                $rx = deg2rad(0);
267 1
                $ry = deg2rad(0);
268 1
                $rz = deg2rad(0);
269 1
                break;
270
271 3
            case RefEll::international1924(): // assumes ED50, values from http://www.globalmapper.com/helpv9/datum_list.htm
272 1
                $tx = -87;
273 1
                $ty = -98;
274 1
                $tz = -121;
275 1
                $s = 0;
276 1
                $rx = deg2rad(0);
277 1
                $ry = deg2rad(0);
278 1
                $rz = deg2rad(0);
279 1
                break;
280
281 2
            case RefEll::bessel1841(): // assumes Germany, values from Wikipedia
282
                $tx = 582;
283
                $ty = -105;
284
                $tz = -414;
285
                $s = 0.0000083;
286
                $rx = deg2rad(1.04 / 3600);
287
                $ry = deg2rad(0.35 / 3600);
288
                $rz = deg2rad(-3.08 / 3600);
289
                break;
290
291
            default:
292 2
                throw new \RuntimeException('Transform parameters not known for this ellipsoid');
293
        }
294
295 5
        return $this->transformDatum(RefEll::wgs84(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
296
    }
297
298
    /**
299
     * Transform co-ordinates from one datum to another using a Helmert transformation.
300
     *
301
     * @param RefEll $toRefEll
302
     * @param float  $tranX
303
     * @param float  $tranY
304
     * @param float  $tranZ
305
     * @param float  $scale
306
     * @param float  $rotX     rotation about x-axis in seconds
307
     * @param float  $rotY     rotation about y-axis in seconds
308
     * @param float  $rotZ     rotation about z-axis in seconds
309
     *
310
     * @return self
311
     */
312 15
    public function transformDatum(RefEll $toRefEll, float $tranX, float $tranY, float $tranZ, float $scale, float $rotX, float $rotY, float $rotZ): self
313
    {
314 15
        if ($this->refEll == $toRefEll) {
315 6
            return $this;
316
        }
317
318 9
        $cartesian = Cartesian::fromLatLong($this);
319 9
        $newCartesian = $cartesian->transformDatum($toRefEll, $tranX, $tranY, $tranZ, $scale, $rotX, $rotY, $rotZ);
320
321 9
        return $newCartesian->toLatitudeLongitude();
322
    }
323
324
    /**
325
     * Convert this LatLng object into an OSGB grid reference. Note that this
326
     * function does not take into account the bounds of the OSGB grid -
327
     * beyond the bounds of the OSGB grid, the resulting OSRef object has no
328
     * meaning.
329
     *
330
     * Reference values for transformation are taken from OS document
331
     * "A Guide to Coordinate Systems in Great Britain".
332
     *
333
     * @return OSRef
334
     */
335 3
    public function toOSRef(): OSRef
336
    {
337 3
        $asOSGB36 = $this->toOSGB36();
338
339 2
        $OSGB = new OSRef(0, 0); //dummy to get reference data
340 2
        $scale = $OSGB->getScaleFactor();
341 2
        $N0 = $OSGB->getOriginNorthing();
342 2
        $E0 = $OSGB->getOriginEasting();
343 2
        $phi0 = $OSGB->getOriginLatitude();
344 2
        $lambda0 = $OSGB->getOriginLongitude();
345
346 2
        $coords = $asOSGB36->toTransverseMercatorEastingNorthing($scale, $E0, $N0, $phi0, $lambda0);
347
348 2
        return new OSRef($coords['E'], $coords['N'], $this->h);
0 ignored issues
show
Bug introduced by
$this->h of type double is incompatible with the type integer expected by parameter $z of PHPCoord\OSRef::__construct(). ( Ignorable by Annotation )

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

348
        return new OSRef($coords['E'], $coords['N'], /** @scrutinizer ignore-type */ $this->h);
Loading history...
349
    }
350
351
    /**
352
     * Convert this LatLng object into an ITM grid reference.
353
     *
354
     * @return ITMRef
355
     */
356 1
    public function toITMRef()
357
    {
358 1
        $asWGS84 = $this->toWGS84();
359
360 1
        $ITM = new ITMRef(0, 0); //dummy to get reference data
361 1
        $scale = $ITM->getScaleFactor();
362 1
        $N0 = $ITM->getOriginNorthing();
363 1
        $E0 = $ITM->getOriginEasting();
364 1
        $phi0 = $ITM->getOriginLatitude();
365 1
        $lambda0 = $ITM->getOriginLongitude();
366
367 1
        $coords = $asWGS84->toTransverseMercatorEastingNorthing($scale, $E0, $N0, $phi0, $lambda0);
368
369 1
        return new ITMRef($coords['E'], $coords['N'], $this->h);
0 ignored issues
show
Bug introduced by
$this->h of type double is incompatible with the type integer expected by parameter $z of PHPCoord\ITMRef::__construct(). ( Ignorable by Annotation )

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

369
        return new ITMRef($coords['E'], $coords['N'], /** @scrutinizer ignore-type */ $this->h);
Loading history...
370
    }
371
372
    /**
373
     * Convert a WGS84 latitude and longitude to an UTM reference.
374
     *
375
     * Reference values for transformation are taken from OS document
376
     * "A Guide to Coordinate Systems in Great Britain".
377
     *
378
     * @return UTMRef
379
     */
380 9
    public function toUTMRef(): UTMRef
381
    {
382 9
        $asWGS84 = $this->toWGS84();
383 9
        $lat = $asWGS84->getLat();
384 9
        $lng = $asWGS84->getLng();
385
386 9
        $longitudeZone = (int) (($asWGS84->getLng() + 180) / 6) + 1;
387
388
        // Special zone for Norway
389 9
        if ($lat >= 56 && $lat < 64 && $lng >= 3 && $lng < 12) {
390 1
            $longitudeZone = 32;
391 8
        } elseif ($lat >= 72 && $lat < 84 && $lng >= 0 && $lng < 42) { // Special zones for Svalbard
392 4
            if ($lng < 9) {
393 1
                $longitudeZone = 31;
394 3
            } elseif ($lng < 21) {
395 1
                $longitudeZone = 33;
396 2
            } elseif ($lng < 33) {
397 1
                $longitudeZone = 35;
398
            } else {
399 1
                $longitudeZone = 37;
400
            }
401
        }
402
403 9
        $UTMZone = $this->getUTMLatitudeZoneLetter($asWGS84->getLat());
404
405 7
        $UTM = new UTMRef(0, 0, 0, $UTMZone, $longitudeZone); //dummy to get reference data
406 7
        $scale = $UTM->getScaleFactor();
407 7
        $N0 = $UTM->getOriginNorthing();
408 7
        $E0 = $UTM->getOriginEasting();
409 7
        $phi0 = $UTM->getOriginLatitude();
410 7
        $lambda0 = $UTM->getOriginLongitude();
411
412 7
        $coords = $asWGS84->toTransverseMercatorEastingNorthing($scale, $E0, $N0, $phi0, $lambda0);
413
414 7
        if ($asWGS84->getLat() < 0) {
415 1
            $coords['N'] += 10000000;
416
        }
417
418 7
        return new UTMRef($coords['E'], $coords['N'], $asWGS84->getH(), $UTMZone, $longitudeZone);
419
    }
420
421
    /**
422
     * Work out the UTM latitude zone from the latitude.
423
     *
424
     * @param float $latitude
425
     *
426
     * @return string
427
     */
428 9
    private function getUTMLatitudeZoneLetter(float $latitude): string
429
    {
430 9
        if ($latitude < -80 || $latitude > 84) {
431 2
            throw new \OutOfRangeException('UTM zones do not apply in polar regions');
432
        }
433
434 7
        $zones = 'CDEFGHJKLMNPQRSTUVWXX';
435 7
        $zoneIndex = (int) (($latitude + 80) / 8);
436
437 7
        return $zones[$zoneIndex];
438
    }
439
440
    /**
441
     * Convert a latitude and longitude to easting and northing using a Transverse Mercator projection
442
     * Formula for transformation is taken from OS document
443
     * "A Guide to Coordinate Systems in Great Britain".
444
     *
445
     * @param float $scale          scale factor on central meridian
446
     * @param float $originEasting  easting of true origin
447
     * @param float $originNorthing northing of true origin
448
     * @param float $originLat      latitude of true origin
449
     * @param float $originLong     longitude of true origin
450
     *
451
     * @return array
452
     */
453 10
    public function toTransverseMercatorEastingNorthing(
454
        float $scale,
455
        float $originEasting,
456
        float $originNorthing,
457
        float $originLat,
458
        float $originLong
459
    ): array {
460 10
        $originLat = deg2rad($originLat);
461 10
        $originLong = deg2rad($originLong);
462
463 10
        $lat = deg2rad($this->lat);
464 10
        $sinLat = sin($lat);
465 10
        $cosLat = cos($lat);
466 10
        $tanLat = tan($lat);
467 10
        $tanLatSq = $tanLat ** 2;
468 10
        $long = deg2rad($this->lng);
469
470 10
        $n = ($this->refEll->getMaj() - $this->refEll->getMin()) / ($this->refEll->getMaj() + $this->refEll->getMin());
471 10
        $nSq = $n ** 2;
472 10
        $nCu = $n ** 3;
473
474 10
        $v = $this->refEll->getMaj() * $scale * ((1 - $this->refEll->getEcc() * ($sinLat ** 2)) ** -0.5);
475 10
        $p = $this->refEll->getMaj() * $scale * (1 - $this->refEll->getEcc()) * ((1 - $this->refEll->getEcc() * ($sinLat ** 2)) ** -1.5);
476 10
        $hSq = (($v / $p) - 1);
477
478 10
        $latPlusOrigin = $lat + $originLat;
479 10
        $latMinusOrigin = $lat - $originLat;
480
481 10
        $longMinusOrigin = $long - $originLong;
482
483 10
        $M = $this->refEll->getMin() * $scale
484 10
            * ((1 + $n + 1.25 * ($nSq + $nCu)) * $latMinusOrigin
485 10
                - (3 * ($n + $nSq) + 2.625 * $nCu) * sin($latMinusOrigin) * cos($latPlusOrigin)
486 10
                + 1.875 * ($nSq + $nCu) * sin(2 * $latMinusOrigin) * cos(2 * $latPlusOrigin)
487 10
                - (35 / 24 * $nCu * sin(3 * $latMinusOrigin) * cos(3 * $latPlusOrigin)));
488
489 10
        $I = $M + $originNorthing;
490 10
        $II = $v / 2 * $sinLat * $cosLat;
491 10
        $III = $v / 24 * $sinLat * ($cosLat ** 3) * (5 - $tanLatSq + 9 * $hSq);
492 10
        $IIIA = $v / 720 * $sinLat * ($cosLat ** 5) * (61 - 58 * $tanLatSq + ($tanLatSq ** 2));
493 10
        $IV = $v * $cosLat;
494 10
        $V = $v / 6 * ($cosLat ** 3) * ($v / $p - $tanLatSq);
495 10
        $VI = $v / 120 * ($cosLat ** 5) * (5 - 18 * $tanLatSq + ($tanLatSq ** 2) + 14 * $hSq - 58 * $tanLatSq * $hSq);
496
497 10
        $E = (int) round($originEasting + $IV * $longMinusOrigin + $V * ($longMinusOrigin ** 3) + $VI * ($longMinusOrigin ** 5));
498 10
        $N = (int) round($I + $II * ($longMinusOrigin ** 2) + $III * ($longMinusOrigin ** 4) + $IIIA * ($longMinusOrigin ** 6));
499
500 10
        return ['E' => $E, 'N' => $N];
501
    }
502
}
503