1 | package Geo::Coordinates::RDNAP;
|
---|
2 |
|
---|
3 | use strict;
|
---|
4 | use warnings;
|
---|
5 |
|
---|
6 | use vars qw($VERSION);
|
---|
7 | $VERSION = '0.11';
|
---|
8 |
|
---|
9 | use Carp;
|
---|
10 | use Params::Validate qw/validate BOOLEAN SCALAR/;
|
---|
11 |
|
---|
12 | use Exporter;
|
---|
13 | use vars qw/@ISA @EXPORT_OK/;
|
---|
14 | @ISA = qw/Exporter/;
|
---|
15 | @EXPORT_OK = qw/from_rd to_rd deg dms/;
|
---|
16 |
|
---|
17 | sub deg {
|
---|
18 | my (@in) = @_;
|
---|
19 | my @out;
|
---|
20 |
|
---|
21 | while (my($d, $m, $s) = splice (@in, 0, 3)) {
|
---|
22 | push @out, $d + ($m||0)/60 + ($s||0)/3600;
|
---|
23 | }
|
---|
24 | return @out;
|
---|
25 | }
|
---|
26 |
|
---|
27 | sub dms {
|
---|
28 | return map {int($_), int($_*60)%60, ($_-int($_*60)/60)*3600} @_;
|
---|
29 | }
|
---|
30 |
|
---|
31 | my %a = (
|
---|
32 | '01' => 3236.0331637,
|
---|
33 | 20 => -32.5915821,
|
---|
34 | '02' => -0.2472814,
|
---|
35 | 21 => -0.8501341,
|
---|
36 | '03' => -0.0655238,
|
---|
37 | 22 => -0.0171137,
|
---|
38 | 40 => 0.0052771,
|
---|
39 | 23 => -0.0003859,
|
---|
40 | 41 => 0.0003314,
|
---|
41 | '04' => 0.0000371,
|
---|
42 | 42 => 0.0000143,
|
---|
43 | 24 => -0.0000090,
|
---|
44 | );
|
---|
45 |
|
---|
46 | my %b = (
|
---|
47 | 10 => 5261.3028966,
|
---|
48 | 11 => 105.9780241,
|
---|
49 | 12 => 2.4576469,
|
---|
50 | 30 => -0.8192156,
|
---|
51 | 31 => -0.0560092,
|
---|
52 | 13 => 0.0560089,
|
---|
53 | 32 => -0.0025614,
|
---|
54 | 14 => 0.0012770,
|
---|
55 | 50 => 0.0002574,
|
---|
56 | 33 => -0.0000973,
|
---|
57 | 51 => 0.0000293,
|
---|
58 | 15 => 0.0000291,
|
---|
59 | );
|
---|
60 |
|
---|
61 | my %c = (
|
---|
62 | '01' => 190066.98903,
|
---|
63 | 11 => -11830.85831,
|
---|
64 | 21 => -114.19754,
|
---|
65 | '03' => -32.38360,
|
---|
66 | 31 => -2.34078,
|
---|
67 | 13 => -0.60639,
|
---|
68 | 23 => 0.15774,
|
---|
69 | 41 => -0.04158,
|
---|
70 | '05' => -0.00661,
|
---|
71 | );
|
---|
72 |
|
---|
73 | my %d = (
|
---|
74 | 10 => 309020.31810,
|
---|
75 | '02' => 3638.36193,
|
---|
76 | 12 => -157.95222,
|
---|
77 | 20 => 72.97141,
|
---|
78 | 30 => 59.79734,
|
---|
79 | 22 => -6.43481,
|
---|
80 | '04' => 0.09351,
|
---|
81 | 32 => -0.07379,
|
---|
82 | 14 => -0.05419,
|
---|
83 | 40 => -0.03444,
|
---|
84 | );
|
---|
85 |
|
---|
86 | my %bessel = (
|
---|
87 | a => 6377397.155,
|
---|
88 | e2 => 6674372e-9,
|
---|
89 | f_i => 299.1528128,
|
---|
90 | );
|
---|
91 |
|
---|
92 | my %etrs89 = (
|
---|
93 | a => 6378137,
|
---|
94 | e2 => 6694380e-9,
|
---|
95 | f_i => 298.257222101,
|
---|
96 | );
|
---|
97 |
|
---|
98 | # Transformation parameters from Bessel to ETRS89 with respect to
|
---|
99 | # Amersfoort.
|
---|
100 |
|
---|
101 | my %b2e = (
|
---|
102 | tx => 593.032,
|
---|
103 | ty => 26,
|
---|
104 | tz => 478.741,
|
---|
105 | a => 1.9848e-6,
|
---|
106 | b => -1.7439e-6,
|
---|
107 | c => 9.0587e-6,
|
---|
108 | d => 4.0772e-6,
|
---|
109 | );
|
---|
110 |
|
---|
111 | my %e2b = map {$_ => -$b2e{$_}} keys %b2e;
|
---|
112 |
|
---|
113 | my @amersfoort_b = ( 3903_453.148, 368_135.313, 5012_970.306 );
|
---|
114 | my @amersfoort_e = ( 3904_046.180, 368_161.313, 5013_449.047 );
|
---|
115 |
|
---|
116 | sub from_rd {
|
---|
117 | croak 'Geo::Coordinates::RDNAP::from_rd needs two or three arguments'
|
---|
118 | if (@_ !=2 && @_ != 3);
|
---|
119 |
|
---|
120 | my ($x, $y, $h) = (@_, 0);
|
---|
121 |
|
---|
122 | croak "Geo::Coordinates::RDNAP::from_rd: X out of bounds: $x"
|
---|
123 | if ($x < -7_000 or $x > 300_000);
|
---|
124 | croak "Geo::Coordinates::RDNAP::from_rd: Y out of bounds: $y"
|
---|
125 | if ($y < 289_000 or $y > 629_000);
|
---|
126 |
|
---|
127 | # Use the approximated transformation.
|
---|
128 | # Step 1: RD -> Bessel (spherical coords)
|
---|
129 |
|
---|
130 | $x = ($x/100_000) - 1.55;
|
---|
131 | $y = ($y/100_000) - 4.63;
|
---|
132 |
|
---|
133 | my $lat = (52*60*60) + (9*60) + 22.178;
|
---|
134 | my $lon = (5 *60*60) + (23*60) + 15.5;
|
---|
135 |
|
---|
136 | foreach my $i (keys %a) {
|
---|
137 | my ($m, $n) = split //, $i;
|
---|
138 | $lat += $a{$i} * ($x**$m) * ($y**$n);
|
---|
139 | }
|
---|
140 |
|
---|
141 | foreach my $i (keys %b) {
|
---|
142 | my ($m, $n) = split //, $i;
|
---|
143 | $lon += $b{$i} * ($x**$m) * ($y**$n);
|
---|
144 | }
|
---|
145 |
|
---|
146 | # Step 2: spherical coords -> X, Y, Z
|
---|
147 | my @coords = _ellipsoid_to_cartesian($lat/3600, $lon/3600, $h, \%bessel);
|
---|
148 |
|
---|
149 | # Step 3: Bessel -> ETRS89
|
---|
150 | @coords = _transform_datum( @coords, \%b2e, \@amersfoort_b );
|
---|
151 |
|
---|
152 | # Step 4: X, Y, Z -> spherical coords
|
---|
153 | return _cartesian_to_ellipsoid(@coords, \%etrs89);
|
---|
154 | }
|
---|
155 |
|
---|
156 | sub to_rd {
|
---|
157 | croak 'Geo::Coordinates::RDNAP::to_rd needs two or three arguments'
|
---|
158 | if (@_ !=2 && @_ != 3);
|
---|
159 |
|
---|
160 | my ($lat, $lon, $h) = (@_, 0);
|
---|
161 |
|
---|
162 | # Use the approximated transformation.
|
---|
163 | # Step 1: spherical coords -> X, Y, Z
|
---|
164 | my @coords = _ellipsoid_to_cartesian($lat, $lon, $h, \%etrs89);
|
---|
165 |
|
---|
166 | # Step 2: ETRS89 -> Bessel
|
---|
167 | @coords = _transform_datum( @coords, \%e2b, \@amersfoort_e );
|
---|
168 |
|
---|
169 | # Step 3: X, Y, Z -> spherical coords
|
---|
170 | ($lat, $lon, $h) = _cartesian_to_ellipsoid(@coords, \%bessel);
|
---|
171 |
|
---|
172 | # Step 4: Bessel -> RD'
|
---|
173 |
|
---|
174 | # Convert to units of 10_000 secs; as deltas from Amersfoort.
|
---|
175 | $lat = ($lat * 3600 - ((52*60*60) + (9*60) + 22.178))/10_000;
|
---|
176 | $lon = ($lon * 3600 - ((5 *60*60) + (23*60) + 15.5))/10_000;
|
---|
177 |
|
---|
178 | my $x = 155e3;
|
---|
179 | my $y = 463e3;
|
---|
180 |
|
---|
181 | foreach my $i (keys %c) {
|
---|
182 | my ($m, $n) = split //, $i;
|
---|
183 | $x += $c{$i} * ($lat**$m) * ($lon**$n);
|
---|
184 | }
|
---|
185 |
|
---|
186 | foreach my $i (keys %d) {
|
---|
187 | my ($m, $n) = split //, $i;
|
---|
188 | $y += $d{$i} * ($lat**$m) * ($lon**$n);
|
---|
189 | }
|
---|
190 |
|
---|
191 | croak "Geo::Coordinates::RDNAP::to_rd: X out of bounds: $x"
|
---|
192 | if ($x < -7_000 or $x > 300_000);
|
---|
193 | croak "Geo::Coordinates::RDNAP::to_rd: Y out of bounds: $y"
|
---|
194 | if ($y < 289_000 or $y > 629_000);
|
---|
195 |
|
---|
196 | return ($x, $y, $h);
|
---|
197 | }
|
---|
198 |
|
---|
199 | sub _to_rads {
|
---|
200 | return $_[0] * 2*3.14159_26535_89793 /360;
|
---|
201 | }
|
---|
202 |
|
---|
203 | sub _from_rads {
|
---|
204 | return $_[0] / (2*3.14159_26535_89793) *360;
|
---|
205 | }
|
---|
206 |
|
---|
207 | sub _ellipsoid_to_cartesian {
|
---|
208 | my ($lat, $lon, $h, $para) = @_;
|
---|
209 |
|
---|
210 | my $sinphi = sin(_to_rads($lat));
|
---|
211 | my $cosphi = cos(_to_rads($lat));
|
---|
212 | my $n = $para->{a}/sqrt(1 - $para->{e2}*$sinphi*$sinphi);
|
---|
213 |
|
---|
214 | return (($n+$h)*$cosphi*cos(_to_rads($lon)),
|
---|
215 | ($n+$h)*$cosphi*sin(_to_rads($lon)),
|
---|
216 | ($n*(1-$para->{e2})+$h)*$sinphi );
|
---|
217 | }
|
---|
218 |
|
---|
219 | # Returns (lat, lon, h) in degrees.
|
---|
220 |
|
---|
221 | sub _cartesian_to_ellipsoid {
|
---|
222 | my ($x, $y, $z, $para) = @_;
|
---|
223 |
|
---|
224 | my $lon = atan2($y, $x);
|
---|
225 |
|
---|
226 | my $r = sqrt($x*$x+$y*$y);
|
---|
227 | my $phi = 0;
|
---|
228 | my $n_sinphi = $z;
|
---|
229 | my $n;
|
---|
230 | my $oldphi;
|
---|
231 |
|
---|
232 | do {
|
---|
233 | $oldphi = $phi;
|
---|
234 | $phi = atan2($z + $para->{e2}*$n_sinphi, $r);
|
---|
235 | my $sinphi = sin($phi);
|
---|
236 | $n = $para->{a}/sqrt(1-$para->{e2}*$sinphi*$sinphi);
|
---|
237 | $n_sinphi = $n*$sinphi;
|
---|
238 | } while (abs($oldphi-$phi) > 1e-8);
|
---|
239 |
|
---|
240 | my $h = $r/cos($phi) - $n;
|
---|
241 |
|
---|
242 | return (_from_rads($phi), _from_rads($lon), $h);
|
---|
243 | }
|
---|
244 |
|
---|
245 | sub _transform_datum {
|
---|
246 | my ($x, $y, $z, $t, $centre) = @_;
|
---|
247 |
|
---|
248 | return (
|
---|
249 | $x + $t->{d}*($x-$centre->[0]) + $t->{c}*($y-$centre->[1])
|
---|
250 | - $t->{b}*($z-$centre->[2]) + $t->{tx},
|
---|
251 | $y - $t->{c}*($x-$centre->[0]) + $t->{d}*($y-$centre->[1])
|
---|
252 | + $t->{a}*($z-$centre->[2]) + $t->{ty},
|
---|
253 | $z + $t->{b}*($x-$centre->[0]) - $t->{a}*($y-$centre->[1])
|
---|
254 | + $t->{d}*($z-$centre->[2]) + $t->{tz}
|
---|
255 | );
|
---|
256 | }
|
---|
257 |
|
---|
258 | 1;
|
---|
259 | __END__
|
---|
260 |
|
---|
261 | =head1 NAME
|
---|
262 |
|
---|
263 | Geo::Coordinates::RDNAP - convert to/from Dutch RDNAP coordinate system
|
---|
264 |
|
---|
265 | =head1 SYNOPSIS
|
---|
266 |
|
---|
267 | use Geo::Coordinates::RDNAP qw/from_rd to_rd dd dms/;
|
---|
268 |
|
---|
269 | # RD coordinates and height in meters
|
---|
270 | my ($lat, $lon, $h) = from_rd( 150_000, 480_000, -2.75 );
|
---|
271 |
|
---|
272 | printf "%d %d' %.2f\" %d %d' %.2f\"", dms($lat, $lon);
|
---|
273 |
|
---|
274 | lat/lon coordinates in degrees; height in meters
|
---|
275 | my ($x, $y, $h) = to_rd( 52.75, 6.80, 10 );
|
---|
276 |
|
---|
277 | # equivalent: to_rd( deg(52,45,0, 6,48,0), 10 );
|
---|
278 |
|
---|
279 | =head1 DESCRIPTION
|
---|
280 |
|
---|
281 | This module converts between two coordinate systems: RD-NAP and ETRS89.
|
---|
282 | ETRS89 is a geodesic frame of reference used in Europe, which is
|
---|
283 | approximately equal to the international reference frame WGS84.
|
---|
284 | GPS data. Coordinates in ETRS89 are given in degrees (latitude and
|
---|
285 | longitude) and meters (height above the reference ellipsoid).
|
---|
286 |
|
---|
287 | RD-NAP (or "Amersfoort datum") is a Dutch coordinate system, consisting
|
---|
288 | of the X and Y coordinates of the Rijksdriehoekmeting, used e.g. in
|
---|
289 | topographical maps, and a Z coordinate which is the height above Normaal
|
---|
290 | Amsterdams Peil, the mean sea level at Amsterdam. X, Y, and Z are all
|
---|
291 | expressed in meters (this is a change compared to the previous versions
|
---|
292 | of this module!)
|
---|
293 |
|
---|
294 | These transformations should only be used for locations in or close to
|
---|
295 | the Netherlands.
|
---|
296 |
|
---|
297 | See http://www.rdnap.nl/ for a description of the RD-NAP system;
|
---|
298 | especially http://www.rdnap.nl/download/rdnaptrans.pdf for the formulas
|
---|
299 | used in this module.
|
---|
300 |
|
---|
301 | =head2 Precision
|
---|
302 |
|
---|
303 | This module implements an approximated transformation, which should be
|
---|
304 | accurate to about 25 cm in X and Y, and about 1 meter in the vertical
|
---|
305 | direction, for all locations in the Netherlands. The full
|
---|
306 | transformation, called RDNAPTRANS, is NOT implemented in this module. It
|
---|
307 | takes into account small deviations, measured at more than 5000 points
|
---|
308 | in the Netherlands.
|
---|
309 |
|
---|
310 | Coordinates in ETRS89 deviate from WGS84 and ITRS because the former is
|
---|
311 | coupled to the Eurasian plate, which drifts a few cm per year compared
|
---|
312 | to other plates. The current (2006) difference between these coordinate
|
---|
313 | systems is in the order of 40 cm.
|
---|
314 |
|
---|
315 | =head2 Disclaimer
|
---|
316 |
|
---|
317 | Although this module implements conversion to/from the RD-NAP coordinate
|
---|
318 | system, it is not a product of RDNAP, the cooperation between the
|
---|
319 | Kadaster and Rijkwaterstaat, which maintains this coordinate system.
|
---|
320 |
|
---|
321 | RDNAPTRANS is a trademark, presumably by Kadaster and/or
|
---|
322 | Rijkswaterstaat. This module is not an implementation of RDNAPTRANS. For
|
---|
323 | the official transformation software, visit http://www.rdnap.nl.
|
---|
324 |
|
---|
325 | =head1 FUNCTIONS
|
---|
326 |
|
---|
327 | =over 4
|
---|
328 |
|
---|
329 | =item from_rd( $x, $y, $h )
|
---|
330 |
|
---|
331 | Converts coordinates in the RD-NAP coordinate system to geographical
|
---|
332 | coordinates. The input are the X and Y coordinates in the RD system,
|
---|
333 | given in meters, and optionally the height above NAP in meters.
|
---|
334 |
|
---|
335 | This should only be used for points in or close to the Netherlands. For
|
---|
336 | this area, X should roughly be between 0 and 300_000, and Y between
|
---|
337 | 300_000 and 650_000.
|
---|
338 |
|
---|
339 | The output is a list of three numbers: latitude and longitude in
|
---|
340 | degrees, according to the ETRS89 coordinate system, and height above the
|
---|
341 | ETRS89 reference geoid, in meters.
|
---|
342 |
|
---|
343 | =item to_rd( $lat, $lon, $h )
|
---|
344 |
|
---|
345 | Converts geegraphical coordinates to coordinates in the RD-NAP
|
---|
346 | coordinate system. The input are the latituse and longitude in degrees,
|
---|
347 | and optionally the height above the ETRS89 reference geoid in meters.
|
---|
348 |
|
---|
349 | This should only be used for points in or close to the Netherlands.
|
---|
350 |
|
---|
351 | The output is a list of three numbers: X and Y in the RD system in
|
---|
352 | meters, and the height above NAP in meters.
|
---|
353 |
|
---|
354 | =item deg
|
---|
355 |
|
---|
356 | Helper function to convert degrees/minutes/seconds to decimal degrees.
|
---|
357 | Works only for positive latitude and longitude.
|
---|
358 |
|
---|
359 | =item dms
|
---|
360 |
|
---|
361 | Helper function to convert decimal degrees to degrees/minutes/seconds.
|
---|
362 | Works only for positive latitude and longitude. The returned degrees and
|
---|
363 | minutes are integers; the returned number of seconds can be fractional.
|
---|
364 |
|
---|
365 | When rounding the number of seconds, remember that it wraps at 60 (and
|
---|
366 | so does the number of minutes). One easy way (but perhaps not the
|
---|
367 | fastest) of taking this into account is the following piece of code:
|
---|
368 |
|
---|
369 | ($d, $m, $s) = dms($degrees);
|
---|
370 | $s = int($s + 0.5);
|
---|
371 | ($d, $m, $s) = map {sprintf "%.0f"} dms(deg($d, $m, $s));
|
---|
372 |
|
---|
373 | =back
|
---|
374 |
|
---|
375 | =head1 BUGS
|
---|
376 |
|
---|
377 | None known.
|
---|
378 |
|
---|
379 | =head1 AUTHOR
|
---|
380 |
|
---|
381 | Eugene van der Pijll C<< <pijll@cpan.org> >>
|
---|
382 |
|
---|
383 | =head1 COPYRIGHT
|
---|
384 |
|
---|
385 | Copyright (c) 2006 Eugene van der Pijll.
|
---|
386 | This program is free software; you can redistribute it and/or modify it
|
---|
387 | under the same terms as Perl itself.
|
---|
388 |
|
---|
389 | The full text of the license can be found in the LICENSE file included
|
---|
390 | with this module.
|
---|
391 |
|
---|
392 | =cut
|
---|