Passed
Push — master ( 4e1176...1642c9 )
by Doug
04:30
created

LatLng::toITMRef()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 14
ccs 10
cts 10
cp 1
crap 1
rs 9.9666
c 0
b 0
f 0
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