Passed
Push — master ( 4e1176...1642c9 )
by Doug
04:30
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 37
    public function __construct(float $lat, float $lng, int $height, RefEll $refEll)
47
    {
48 37
        $this->lat = $lat;
49 37
        $this->lng = $lng;
50 37
        $this->h = $height;
51 37
        $this->refEll = $refEll;
52 37
    }
53
54
    /**
55
     * Return a string representation of this LatLng object.
56
     */
57 14
    public function __toString(): string
58
    {
59 14
        return "({$this->getLat()}, {$this->getLng()})";
60
    }
61
62 30
    public function getLat(): float
63
    {
64 30
        return round($this->lat, 5);
65
    }
66
67 30
    public function getLng(): float
68
    {
69 30
        return round($this->lng, 5);
70
    }
71
72 15
    public function getH(): int
73
    {
74 15
        return $this->h;
75
    }
76
77 9
    public function getRefEll(): RefEll
78
    {
79 9
        return $this->refEll;
80
    }
81
82
    /**
83
     * Calculate the surface distance between this LatLng object and the one
84
     * passed in as a parameter.
85
     *
86
     * @param LatLng $to a LatLng object to measure the surface distance to
87
     *
88
     * @return int distance in metres
89
     */
90 2
    public function distance(self $to): int
91
    {
92 2
        if ($this->refEll != $to->refEll) {
93 1
            throw new \RuntimeException('Source and destination co-ordinates are not using the same ellipsoid');
94
        }
95
96
        //Mean radius definition taken from Wikipedia
97 1
        $er = ((2 * $this->refEll->getMaj()) + $this->refEll->getMin()) / 3;
98
99 1
        $latFrom = deg2rad($this->lat);
100 1
        $latTo = deg2rad($to->lat);
101 1
        $lngFrom = deg2rad($this->lng);
102 1
        $lngTo = deg2rad($to->lng);
103
104 1
        $d = acos(sin($latFrom) * sin($latTo) + cos($latFrom) * cos($latTo) * cos($lngTo - $lngFrom)) * $er;
105
106 1
        return (int) round($d);
107
    }
108
109
    /**
110
     * Convert this LatLng object to OSGB36 datum.
111
     * Reference values for transformation are taken from OS document
112
     * "A Guide to Coordinate Systems in Great Britain".
113
     */
114 4
    public function toOSGB36(): self
115
    {
116 4
        if ($this->refEll == RefEll::airy1830()) {
117 2
            return $this;
118
        }
119
120 2
        $asWGS84 = $this->toWGS84();
121
122 1
        $tx = -446.448;
123 1
        $ty = 125.157;
124 1
        $tz = -542.060;
125 1
        $s = 0.0000204894;
126 1
        $rx = deg2rad(-0.1502 / 3600);
127 1
        $ry = deg2rad(-0.2470 / 3600);
128 1
        $rz = deg2rad(-0.8421 / 3600);
129
130 1
        return $asWGS84->transformDatum(RefEll::airy1830(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
131
    }
132
133
    /**
134
     * Convert this LatLng object to ED50 datum.
135
     * Reference values for transformation are taken from http://www.globalmapper.com/helpv9/datum_list.htm .
136
     */
137 1
    public function toED50(): self
138
    {
139 1
        if ($this->refEll == RefEll::international1924()) {
140
            return $this;
141
        }
142
143 1
        $asWGS84 = $this->toWGS84();
144
145 1
        $tx = 87;
146 1
        $ty = 98;
147 1
        $tz = 121;
148 1
        $s = 0;
149 1
        $rx = deg2rad(0);
150 1
        $ry = deg2rad(0);
151 1
        $rz = deg2rad(0);
152
153 1
        return $asWGS84->transformDatum(RefEll::international1924(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
154
    }
155
156
    /**
157
     * Convert this LatLng object to NAD27 datum.
158
     * Reference values for transformation are taken from Wikipedia.
159
     */
160 1
    public function toNAD27(): self
161
    {
162 1
        if ($this->refEll == RefEll::clarke1866()) {
163
            return $this;
164
        }
165
166 1
        $asWGS84 = $this->toWGS84();
167
168 1
        $tx = 8;
169 1
        $ty = -160;
170 1
        $tz = -176;
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::clarke1866(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
177
    }
178
179
    /**
180
     * Convert this LatLng object to Ireland 1975 datum.
181
     * Reference values for transformation are taken from OSI document
182
     * "Making maps compatible with GPS".
183
     */
184 1
    public function toIreland1975(): self
185
    {
186 1
        if ($this->refEll == RefEll::airyModified()) {
187
            return $this;
188
        }
189
190 1
        $asWGS84 = $this->toWGS84();
191
192 1
        $tx = -482.530;
193 1
        $ty = 130.596;
194 1
        $tz = -564.557;
195 1
        $s = -0.00000815;
196 1
        $rx = deg2rad(-1.042 / 3600);
197 1
        $ry = deg2rad(-0.214 / 3600);
198 1
        $rz = deg2rad(-0.631 / 3600);
199
200 1
        return $asWGS84->transformDatum(RefEll::airyModified(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
201
    }
202
203
    /**
204
     * Convert this LatLng object to WGS84 datum.
205
     */
206 21
    public function toWGS84(): self
207
    {
208 21
        switch ($this->refEll) {
209 21
            case RefEll::wgs84():
210 13
                return $this; //do nothing
211
212 8
            case RefEll::grs80(): //assumes ITM, GRS80 vs WGS84 is well within accuracy tolerance (<1mm) so don't need to do a Helmert
213 1
                $this->refEll = RefEll::wgs84();
214 1
                return $this;
215
216 7
            case RefEll::airy1830(): // values from OSGB document "A Guide to Coordinate Systems in Great Britain"
217 2
                $tx = 446.448;
218 2
                $ty = -125.157;
219 2
                $tz = 542.060;
220 2
                $s = -0.0000204894;
221 2
                $rx = deg2rad(0.1502 / 3600);
222 2
                $ry = deg2rad(0.2470 / 3600);
223 2
                $rz = deg2rad(0.8421 / 3600);
224 2
                break;
225
226 5
            case RefEll::airyModified(): // values from OSI document "Making maps compatible with GPS"
227 1
                $tx = 482.530;
228 1
                $ty = -130.596;
229 1
                $tz = 564.557;
230 1
                $s = 0.00000815;
231 1
                $rx = deg2rad(1.042 / 3600);
232 1
                $ry = deg2rad(0.214 / 3600);
233 1
                $rz = deg2rad(0.631 / 3600);
234 1
                break;
235
236 4
            case RefEll::clarke1866(): // assumes NAD27, values from Wikipedia
237 1
                $tx = -8;
238 1
                $ty = 160;
239 1
                $tz = 176;
240 1
                $s = 0;
241 1
                $rx = deg2rad(0);
242 1
                $ry = deg2rad(0);
243 1
                $rz = deg2rad(0);
244 1
                break;
245
246 3
            case RefEll::international1924(): // assumes ED50, values from http://www.globalmapper.com/helpv9/datum_list.htm
247 1
                $tx = -87;
248 1
                $ty = -98;
249 1
                $tz = -121;
250 1
                $s = 0;
251 1
                $rx = deg2rad(0);
252 1
                $ry = deg2rad(0);
253 1
                $rz = deg2rad(0);
254 1
                break;
255
256 2
            case RefEll::bessel1841(): // assumes Germany, values from Wikipedia
257
                $tx = 582;
258
                $ty = -105;
259
                $tz = -414;
260
                $s = 0.0000083;
261
                $rx = deg2rad(1.04 / 3600);
262
                $ry = deg2rad(0.35 / 3600);
263
                $rz = deg2rad(-3.08 / 3600);
264
                break;
265
266
            default:
267 2
                throw new \RuntimeException('Transform parameters not known for this ellipsoid');
268
        }
269
270 5
        return $this->transformDatum(RefEll::wgs84(), $tx, $ty, $tz, $s, $rx, $ry, $rz);
271
    }
272
273
    /**
274
     * Transform co-ordinates from one datum to another using a Helmert transformation.
275
     *
276
     * @param  RefEll $toRefEll Reference Ellipsoid
277
     * @param  float  $tranX    Translation in x-axis in metres
278
     * @param  float  $tranY    Translation in y-axis in metres
279
     * @param  float  $tranZ    Translation in z-axis in metres
280
     * @param  float  $scale    Scale factor
281
     * @param  float  $rotX     rotation about x-axis in seconds
282
     * @param  float  $rotY     rotation about y-axis in seconds
283
     * @param  float  $rotZ     rotation about z-axis in seconds
284
     * @return LatLng
285
     */
286 15
    public function transformDatum(RefEll $toRefEll, float $tranX, float $tranY, float $tranZ, float $scale, float $rotX, float $rotY, float $rotZ): self
287
    {
288 15
        if ($this->refEll == $toRefEll) {
289 6
            return $this;
290
        }
291
292 9
        $cartesian = Cartesian::fromLatLong($this);
293 9
        $newCartesian = $cartesian->transformDatum($toRefEll, $tranX, $tranY, $tranZ, $scale, $rotX, $rotY, $rotZ);
294
295 9
        return $newCartesian->toLatitudeLongitude();
296
    }
297
298
    /**
299
     * Convert this LatLng object into an OSGB grid reference. Note that this
300
     * function does not take into account the bounds of the OSGB grid -
301
     * beyond the bounds of the OSGB grid, the resulting OSRef object has no
302
     * meaning.
303
     *
304
     * Reference values for transformation are taken from OS document
305
     * "A Guide to Coordinate Systems in Great Britain".
306
     */
307 3
    public function toOSRef(): OSRef
308
    {
309 3
        $asOSGB36 = $this->toOSGB36();
310
311 2
        $OSGB = new OSRef(0, 0); //dummy to get reference data
312 2
        $scale = $OSGB->getScaleFactor();
313 2
        $N0 = $OSGB->getOriginNorthing();
314 2
        $E0 = $OSGB->getOriginEasting();
315 2
        $phi0 = $OSGB->getOriginLatitude();
316 2
        $lambda0 = $OSGB->getOriginLongitude();
317
318 2
        $coords = $asOSGB36->toTransverseMercatorEastingNorthing($scale, $E0, $N0, $phi0, $lambda0);
319
320 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

320
        return new OSRef($coords['E'], $coords['N'], /** @scrutinizer ignore-type */ $this->h);
Loading history...
321
    }
322
323
    /**
324
     * Convert this LatLng object into an ITM grid reference.
325
     *
326
     * @return ITMRef
327
     */
328 1
    public function toITMRef()
329
    {
330 1
        $asWGS84 = $this->toWGS84();
331
332 1
        $ITM = new ITMRef(0, 0); //dummy to get reference data
333 1
        $scale = $ITM->getScaleFactor();
334 1
        $N0 = $ITM->getOriginNorthing();
335 1
        $E0 = $ITM->getOriginEasting();
336 1
        $phi0 = $ITM->getOriginLatitude();
337 1
        $lambda0 = $ITM->getOriginLongitude();
338
339 1
        $coords = $asWGS84->toTransverseMercatorEastingNorthing($scale, $E0, $N0, $phi0, $lambda0);
340
341 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

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