File Coverage

lib/Geo/HelmertTransform.pm
Criterion Covered Total %
statement 67 77 87.0
branch 6 16 37.5
condition 0 3 0.0
subroutine 21 21 100.0
pod 6 6 100.0
total 100 123 81.3


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             #
3             # Geo/HelmertTransform.pm:
4             # Perform "Helmert" (linear) transformations between coordinates referenced to
5             # different datums.
6             #
7             # Reference:
8             # http://www.gps.gov.uk/additionalInfo/images/A_guide_to_coord.pdf
9             #
10             # Copyright (c) 2005 UK Citizens Online Democracy. This module is free
11             # software; you can redistribute it and/or modify it under the same terms as
12             # Perl itself.
13             #
14             # Email: team@mysociety.org; WWW: http://www.mysociety.org/
15             #
16             # $Id: HelmertTransform.pm,v 1.15 2011-08-10 09:38:42 evdb Exp $
17             #
18              
19             package Geo::HelmertTransform;
20              
21             $Geo::HelmertTransform::VERSION = '1.14';
22              
23 1     1   751 use strict;
  1         2  
  1         63  
24              
25             =head1 NAME
26              
27             Geo::HelmertTransform
28              
29             =head1 VERSION
30              
31             1.14
32              
33             =head1 SYNOPSIS
34              
35             use Geo::HelmertTransform;
36              
37             my ($lat, $lon, $h) = ...; # from OS map
38             my $airy1830 = Geo::HelmertTransform::datum('Airy1830');
39             my $wgs84 = Geo::HelmertTransform::datum('WGS84');
40              
41             ($lat, $lon, $h)
42             = Geo::HelmertTransform::convert_datum($airy1830, $wgs84,
43             $lat, $lon, $h);
44              
45              
46             =head1 DESCRIPTION
47              
48             Perform transformations between geographical coordinates in different datums.
49              
50             It is usual to describe geographical points in terms of their polar coordinates
51             (latitude, longitude and altitude) referenced to a "datum ellipsoid", which is
52             used to approximate the Earth's geoid. The latitude, longitude and altitude of
53             a given physical point vary depending on which datum ellipsoid is in use.
54             Unfortunately, a number of ellipsoids are in everyday use, and so it is often
55             necessary to transform geographical coordinates between different datum
56             ellipsoids.
57              
58             Two different datum ellipsoids may differ in the locations of their centers, or
59             in their shape; and there may be an angle between their equatorial planes or
60             the meridians relative to which longitude is measured. The Helmert Transform,
61             which this module implements, is a linear transformation of coordinates between
62             pairs of datum ellipsoids in the limit of small angles of deviation between
63             them.
64              
65             =head1 CONVENTIONS
66              
67             Latitude is expressed in degrees, positive-north; longitude in degrees,
68             positive-east. Heights (ellipsoid) and cartesian coordinates are in meters.
69              
70             =head1 FUNCTIONS
71              
72             =over 4
73              
74             =cut
75              
76 1     1   8 use constant M_PI => 3.141592654;
  1         3  
  1         808  
77              
78             =item rad_to_deg RADIANS
79              
80             Convert RADIANS to degrees.
81              
82             =cut
83             sub rad_to_deg ($) {
84 2     2 1 31 return 180. * $_[0] / M_PI;
85             }
86              
87             =item deg_to_rad DEGREES
88              
89             Convert DEGREES to radians.
90              
91             =cut
92             sub deg_to_rad ($) {
93 8     8 1 21 return M_PI * $_[0] / 180.;
94             }
95              
96             =item geo_to_xyz DATUM LAT LON H
97              
98             Return the Cartesian (X, Y, Z) coordinates for the geographical coordinates
99             (LAT, LON, H) in the given DATUM.
100              
101             =cut
102             sub geo_to_xyz ($$$$) {
103 1     1 1 3 my ($datum, $lat, $lon, $h) = @_;
104 1         2 $lat = deg_to_rad($lat);
105 1         3 $lon = deg_to_rad($lon);
106            
107 1         38 my $v = $datum->a() / sqrt(1 - $datum->e2() * sin($lat) ** 2);
108             return (
109 1         15 ($v + $h) * cos($lat) * cos($lon),
110             ($v + $h) * cos($lat) * sin($lon),
111             ((1 - $datum->e2()) * $v + $h) * sin($lat)
112             );
113             }
114              
115             =item xyz_to_geo DATUM X Y Z
116              
117             Return the geographical (LAT, LON, H) coordinates for the Cartesian coordinates
118             (X, Y, Z) in the given DATUM. This is an iterative procedure.
119              
120             =cut
121             sub xyz_to_geo ($$$$) {
122 1     1 1 3 my ($datum, $x, $y, $z) = @_;
123 1         2 my ($lat, $lat2, $lon, $h, $v, $p);
124 1         18 $lon = atan2($y, $x);
125            
126 1         4 $p = sqrt($x**2 + $y**2);
127 1         1 $lat2 = atan2($z, $p);
128              
129 1         3 my $niter = 0;
130 1         1 do {
131 1         2 $lat = $lat2;
132 1         18 $v = $datum->a() / sqrt(1 - $datum->e2() * sin($lat) ** 2);
133 1         5 $lat2 = atan2(($z + $datum->e2() * $v * sin($lat)), $p);
134 1 50       18 die "exceeded 10000 iterations without converging in Geo::HelmertTransform::xyz_to_geo"
135             if (++$niter > 10000);
136             } while (abs($lat2 - $lat) > 2e-6); # about 1/10000 mile
137              
138 1         10 $h = $p / cos($lat) - $v;
139              
140 1         5 return (rad_to_deg($lat), rad_to_deg($lon), $h);
141             }
142              
143             =item convert_datum D1 D2 LAT LON H
144              
145             Given geographical coordinates (LAT, LON, H) in datum D1, return the
146             corresponding coordinates in datum D2. This assumes that the transformations
147             are small, and always converts via WGS84.
148              
149             =cut
150             sub convert_datum ($$$$$) {
151 1     1 1 6 my ($d1, $d2, $lat, $lon, $h) = @_;
152 1         4 my ($x1, $y1, $z1) = geo_to_xyz($d1, $lat, $lon, $h);
153 1         2 my ($x, $y, $z) = ($x1, $y1, $z1);
154 1 50       18 if (!$d1->is_wgs84()) {
155             # Transform into WGS84.
156 1         19 $x = $d1->tx()
157             + (1 + $d1->s()) * $x1
158             - $d1->rz() * $y1
159             + $d1->ry() * $z1;
160 1         19 $y = $d1->ty()
161             + $d1->rz() * $x1
162             + (1 + $d1->s()) * $y1
163             - $d1->rx() * $z1;
164 1         18 $z = $d1->tz()
165             - $d1->ry() * $x1
166             + $d1->rx() * $y1
167             + (1 + $d1->s()) * $z1;
168             }
169              
170 1         4 my ($x2, $y2, $z2) = ($x, $y, $z);
171 1 50       19 if (!$d2->is_wgs84()) {
172 0         0 $x2 = -$d2->tx()
173             + (1 - $d2->s()) * $x
174             + $d2->rz() * $y
175             - $d2->ry() * $z;
176 0         0 $y2 = -$d2->ty()
177             - $d2->rz() * $x
178             + (1 - $d2->s()) * $y
179             + $d2->rx() * $z;
180 0         0 $z2 = -$d2->tz()
181             + $d2->ry() * $x
182             - $d2->rx() * $y
183             + (1 - $d2->s()) * $z;
184             }
185              
186 1         4 return xyz_to_geo($d2, $x2, $y2, $z2);
187             }
188              
189             =item datum NAME
190              
191             Return the datum of the given NAME. Currently implemented are:
192              
193             =over 4
194              
195             =item Airy1830
196              
197             The 1830 Airy ellipsoid to which the British Ordnance Survey's National Grid is
198             referenced.
199              
200             =item Airy1830Modified
201              
202             The modified 1830 Airy ellipsoid to which the Irish Grid (as used by Ordnance
203             Survey Ireland and Ordnance Survey Northern Ireland); also known as the Ireland
204             1975 datum.
205              
206             =item WGS84
207              
208             The global datum used for GPS.
209              
210             =back
211              
212             =cut
213             sub datum ($) {
214 2     2 1 404 return new Geo::HelmertTransform::Datum(Name => $_[0]);
215             }
216              
217             =back
218              
219             =cut
220              
221             # Datum class for internal use (alternative spelling: "I can't be bothered to
222             # document it now").
223             package Geo::HelmertTransform::Datum;
224              
225 1     1   1039 use fields qw(name a b e2 tx ty tz s rx ry rz is_wgs84);
  1         1519  
  1         6  
226              
227             # Fields are: semi-major and -minor axes; and the x-, y-, and z-displacements,
228             # scale change, and rotations to transform from this datum into WGS84.
229             #
230             # a (m) b tx ty tz s (ppm) rx (sec) ry rz
231             # -------------- --------------- --------- --------- --------- --------- -------- -------- -------
232             my %known_datums = (
233             # from OS article above
234             Airy1830 => [6_377_563.396, 6_356_256.910, +446.448, -125.157, +542.060, -20.4894, +0.1502, +0.2470, +0.8421],
235             # from http://www.osni.gov.uk/downloads/Making%20maps%20GPS%20compatible.pdf
236             Airy1830Modified => [6_377_340.189, 6_356_034.447, +482.530, -130.596, +564.557, +8.150, -1.042, -0.214, -0.631],
237             # International1924 => [6_378_388.000, 6_356_911.946, ??? ],
238             WGS84 => [6_378_137.000, 6_356_752.3141, 0.000, 0.000, 0.000, 0.0000, 0.0000, 0.0000, 0.0000]
239             );
240              
241             sub new ($%) {
242 2     2   6 my ($class, %p) = @_;
243 2 50 0     8 if (exists($p{Name})) {
    0          
244 2 50       8 die "datum \"$p{Name}\" not known"
245             if (!exists($known_datums{$p{Name}}));
246 2         2 my @d = @{$known_datums{$p{Name}}};
  2         7  
247 2         9 my $s = fields::new($class);
248 2         4700 foreach (qw(a b tx ty tz)) {
249 10         22 $s->{$_} = shift(@d);
250             }
251 2         7 $s->{s} = shift(@d) / 1_000_000; # ppm
252 2         5 foreach (qw(rx ry rz)) {
253 6         15 $s->{$_} = Geo::HelmertTransform::deg_to_rad(shift(@d) / 3600.); # seconds
254             }
255 2         9 $s->{is_wgs84} = ($p{Name} eq 'WGS84');
256 2         9 return $s;
257             } elsif (!exists($p{a}) || !exists($p{b})) {
258 0         0 die "must specify semi-major axis a and semi-minor axis b";
259             } else {
260 0         0 my $s = fields::new($class);
261 0         0 foreach (qw(a b tx ty tz s rx ry rz)) {
262 0         0 $s->{$_} = 0;
263 0 0       0 $s->{$_} = $p{$_} if (exists($p{$_}));
264             }
265 0         0 $s->{is_wgs84} = 0;
266 0         0 return $s;
267             }
268             }
269              
270             foreach (qw(a b tx ty tz s rx ry rz is_wgs84)) {
271 6     6   38 eval <
  4     4   67  
  2     2   11  
  2     2   21  
  2     2   23  
  2     2   38  
  3     3   47  
  1     1   18  
  1     1   20  
  1     1   17  
272             sub $_ (\$) {
273             return \$_[0]->{$_};
274             }
275             EOF
276             }
277              
278             sub e2 ($) {
279 4     4   5 my $s = shift;
280 4 50       14 if (!exists($_[0]->{e2})) {
281 4         70 $s->{e2} = 1 - ($s->b() / $s->a()) ** 2;
282             }
283 4         28 return $s->{e2}
284             }
285              
286             =head1 SEE ALSO
287              
288             I,
289             http://www.gps.gov.uk/guidecontents.asp
290              
291             I,
292             http://www.osni.gov.uk/downloads/Making%20maps%20GPS%20compatible.pdf
293              
294             =head1 AUTHOR AND COPYRIGHT
295              
296             Written by Chris Lightfoot, team@mysociety.org
297              
298             Copyright (c) UK Citizens Online Democracy.
299              
300             This module is free software; you can redistribute it and/or modify it under
301             the same terms as Perl itself.
302              
303             =cut
304              
305             1;
306