| 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
|
---|