File Coverage

blib/lib/Geo/SweGrid.pm
Criterion Covered Total %
statement 82 175 46.8
branch 5 56 8.9
condition n/a
subroutine 6 8 75.0
pod 3 3 100.0
total 96 242 39.6


line stmt bran cond sub pod time code
1             package Geo::SweGrid;
2              
3 1     1   28982 use strict;
  1         2  
  1         43  
4 1     1   1872 use Math::Trig;
  1         26189  
  1         2571  
5              
6             our $VERSION = "1.0";
7              
8             =head1 NAME
9              
10             Geo::SweGrid - Convert coordinates between geodetic WGS84 and Swedish grid RT90 and SWEREF99 systems
11              
12             =head1 SYNOPSIS
13              
14             use Geo::SweGrid;
15              
16             my $grid = Geo::SweGrid->new("rt90_2.5_gon_v") or die "No grid, projection not recognized";
17              
18             my ($lat, $lon) = $grid->grid_to_geodetic(7011002, 1299996);
19              
20             my ($x, $y) = $grid->geodetic_to_grid(63.1530261140462, 11.8353976399345);
21              
22              
23             =head1 DESCRIPTION
24              
25             Convert coordinates between geodetic WGS84 and Swedish grid RT90 and SWEREF99 systems.
26              
27             Implementation of "Gauss Conformal Projection (Transverse Mercator), Krügers Formulas".
28              
29             Parameters for SWEREF99 lat-long to/from RT90 and SWEREF99
30             coordinates (RT90 and SWEREF99 are used in Swedish maps).
31              
32              
33             =over 2
34              
35             =item $grid = Geo::SweGrid->new($projection)
36              
37             Constructor for Geo::SweGrid
38              
39             Creates an instance and sets up parameters for the given RT90 or SWEREF99TM projection.
40             Note: Parameters for RT90 are choosen to eliminate the
41             differences between Bessel and GRS80-ellipsoides.
42             Bessel-variants should only be used if lat/long are given as
43             RT90-lat/long based on the Bessel ellipsoide (from old maps).
44              
45             Parameters:
46              
47             $projection (string). Must match a recognized projection.
48              
49             List of supported projections:
50              
51             rt90_0.0_gon_v
52             rt90_2.5_gon_o
53             rt90_2.5_gon_v
54             rt90_5.0_gon_o
55             rt90_5.0_gon_v
56             rt90_7.5_gon_v
57             bessel_rt90_0.0_gon_v
58             bessel_rt90_2.5_gon_o
59             bessel_rt90_2.5_gon_v
60             bessel_rt90_5.0_gon_o
61             bessel_rt90_5.0_gon_v
62             bessel_rt90_7.5_gon_v
63             sweref_99_1200
64             sweref_99_1330
65             sweref_99_1415
66             sweref_99_1500
67             sweref_99_1545
68             sweref_99_1630
69             sweref_99_1715
70             sweref_99_1800
71             sweref_99_1845
72             sweref_99_2015
73             sweref_99_2145
74             sweref_99_2315
75             sweref_99_tm
76              
77              
78             Example:
79              
80             my $grid = Geo::SweGrid->new("rt90_2.5_gon_v") or die "No grid, projection not recognized";
81              
82             Returns:
83              
84             the created instance;
85              
86             =cut
87              
88             sub new {
89 1     1 1 11 my $class = shift;
90 1         3 my $projection = shift;
91 1         3 my $self = bless {}, $class;
92              
93 1         6 $self->{axis} = undef; # Semi-major axis of the ellipsoid.
94 1         4 $self->{flattening} = undef; # Flattening of the ellipsoid.
95 1         2 $self->{central_meridian} = undef; # Central meridian for the projection.
96 1         2 $self->{lat_of_origin} = undef; # Latitude of origin.
97 1         3 $self->{scale} = undef; # Scale on central meridian.
98 1         2 $self->{false_northing} = undef; # Offset for origo.
99 1         2 $self->{false_easting} = undef; # Offset for origo.
100              
101             # RT90 parameters, GRS 80 ellipsoid.
102 1 50       9 if ($projection eq "rt90_7.5_gon_v") {
    50          
    50          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
103 0         0 $self->_grs80_params();
104 0         0 $self->{central_meridian} = 11.0 + 18.375/60.0;
105 0         0 $self->{scale} = 1.000006000000;
106 0         0 $self->{false_northing} = -667.282;
107 0         0 $self->{false_easting} = 1500025.141;
108             }
109             elsif ($projection eq "rt90_5.0_gon_v") {
110 0         0 $self->_grs80_params();
111 0         0 $self->{central_meridian} = 13.0 + 33.376/60.0;
112 0         0 $self->{scale} = 1.000005800000;
113 0         0 $self->{false_northing} = -667.130;
114 0         0 $self->{false_easting} = 1500044.695;
115             }
116             elsif ($projection eq "rt90_2.5_gon_v") {
117 1         97 $self->_grs80_params();
118 1         3 $self->{central_meridian} = 15.0 + 48.0/60.0 + 22.624306/3600.0;
119 1         3 $self->{scale} = 1.00000561024;
120 1         2 $self->{false_northing} = -667.711;
121 1         3 $self->{false_easting} = 1500064.274;
122             }
123             elsif ($projection eq "rt90_0.0_gon_v") {
124 0         0 $self->_grs80_params();
125 0         0 $self->{central_meridian} = 18.0 + 3.378/60.0;
126 0         0 $self->{scale} = 1.000005400000;
127 0         0 $self->{false_northing} = -668.844;
128 0         0 $self->{false_easting} = 1500083.521;
129             }
130             elsif ($projection eq "rt90_2.5_gon_o") {
131 0         0 $self->_grs80_params();
132 0         0 $self->{central_meridian} = 20.0 + 18.379/60.0;
133 0         0 $self->{scale} = 1.000005200000;
134 0         0 $self->{false_northing} = -670.706;
135 0         0 $self->{false_easting} = 1500102.765;
136             }
137             elsif ($projection eq "rt90_5.0_gon_o") {
138 0         0 $self->_grs80_params();
139 0         0 $self->{central_meridian} = 22.0 + 33.380/60.0;
140 0         0 $self->{scale} = 1.000004900000;
141 0         0 $self->{false_northing} = -672.557;
142 0         0 $self->{false_easting} = 1500121.846;
143             }
144              
145             # RT90 parameters, Bessel 1841 ellipsoid.
146             elsif ($projection eq "bessel_rt90_7.5_gon_v") {
147 0         0 $self->_bessel_params();
148 0         0 $self->{central_meridian} = 11.0 + 18.0/60.0 + 29.8/3600.0;
149             }
150             elsif ($projection eq "bessel_rt90_5.0_gon_v") {
151 0         0 $self->_bessel_params();
152 0         0 $self->{central_meridian} = 13.0 + 33.0/60.0 + 29.8/3600.0;
153             }
154             elsif ($projection eq "bessel_rt90_2.5_gon_v") {
155 0         0 $self->_bessel_params();
156 0         0 $self->{central_meridian} = 15.0 + 48.0/60.0 + 29.8/3600.0;
157             }
158             elsif ($projection eq "bessel_rt90_0.0_gon_v") {
159 0         0 $self->_bessel_params();
160 0         0 $self->{central_meridian} = 18.0 + 3.0/60.0 + 29.8/3600.0;
161             }
162             elsif ($projection eq "bessel_rt90_2.5_gon_o") {
163 0         0 $self->_bessel_params();
164 0         0 $self->{central_meridian} = 20.0 + 18.0/60.0 + 29.8/3600.0;
165             }
166             elsif ($projection eq "bessel_rt90_5.0_gon_o") {
167 0         0 $self->_bessel_params();
168 0         0 $self->{central_meridian} = 22.0 + 33.0/60.0 + 29.8/3600.0;
169             }
170              
171             # SWEREF99TM and SWEREF99ddmm parameters.
172             elsif ($projection eq "sweref_99_tm") {
173 0         0 $self->_sweref99_params();
174 0         0 $self->{central_meridian} = 15.00;
175 0         0 $self->{lat_of_origin} = 0.0;
176 0         0 $self->{scale} = 0.9996;
177 0         0 $self->{false_northing} = 0.0;
178 0         0 $self->{false_easting} = 500000.0;
179             }
180             elsif ($projection eq "sweref_99_1200") {
181 0         0 $self->_sweref99_params();
182 0         0 $self->{central_meridian} = 12.00;
183             }
184             elsif ($projection eq "sweref_99_1330") {
185 0         0 $self->_sweref99_params();
186 0         0 $self->{central_meridian} = 13.50;
187             }
188             elsif ($projection eq "sweref_99_1500") {
189 0         0 $self->_sweref99_params();
190 0         0 $self->{central_meridian} = 15.00;
191             }
192             elsif ($projection eq "sweref_99_1630") {
193 0         0 $self->_sweref99_params();
194 0         0 $self->{central_meridian} = 16.50;
195             }
196             elsif ($projection eq "sweref_99_1800") {
197 0         0 $self->_sweref99_params();
198 0         0 $self->{central_meridian} = 18.00;
199             }
200             elsif ($projection eq "sweref_99_1415") {
201 0         0 $self->_sweref99_params();
202 0         0 $self->{central_meridian} = 14.25;
203             }
204             elsif ($projection eq "sweref_99_1545") {
205 0         0 $self->_sweref99_params();
206 0         0 $self->{central_meridian} = 15.75;
207             }
208             elsif ($projection eq "sweref_99_1715") {
209 0         0 $self->_sweref99_params();
210 0         0 $self->{central_meridian} = 17.25;
211             }
212             elsif ($projection eq "sweref_99_1845") {
213 0         0 $self->_sweref99_params();
214 0         0 $self->{central_meridian} = 18.75;
215             }
216             elsif ($projection eq "sweref_99_2015") {
217 0         0 $self->_sweref99_params();
218 0         0 $self->{central_meridian} = 20.25;
219             }
220             elsif ($projection eq "sweref_99_2145") {
221 0         0 $self->_sweref99_params();
222 0         0 $self->{central_meridian} = 21.75;
223             }
224             elsif ($projection eq "sweref_99_2315") {
225 0         0 $self->_sweref99_params();
226 0         0 $self->{central_meridian} = 23.25;
227             }
228              
229             # Test-case:
230             # Lat: 66 0'0", lon: 24 0'0".
231             # X:1135809.413803 Y:555304.016555.
232             elsif ($projection eq "test_case") {
233 0         0 $self->{axis} = 6378137.0;
234 0         0 $self->{flattening} = 1.0 / 298.257222101;
235 0         0 $self->{central_meridian} = 13.0 + 35.0/60.0 + 7.692000/3600.0;
236 0         0 $self->{lat_of_origin} = 0.0;
237 0         0 $self->{scale} = 1.000002540000;
238 0         0 $self->{false_northing} = -6226307.8640;
239 0         0 $self->{false_easting} = 84182.8790;
240              
241             # Not a valid $projection.
242             } else {
243 0         0 return undef;
244             }
245              
246 1         4 return $self;
247             }
248              
249             # Sets of default parameters.
250             sub _grs80_params() {
251 1     1   2 my $self = shift;
252 1         3 $self->{axis} = 6378137.0; # GRS 80.
253 1         2 $self->{flattening} = 1.0 / 298.257222101; # GRS 80.
254 1         3 $self->{central_meridian} = undef;
255 1         2 $self->{lat_of_origin} = 0.0;
256             }
257              
258             sub _bessel_params() {
259 0     0   0 my $self = shift;
260 0         0 $self->{axis} = 6377397.155; # Bessel 1841.
261 0         0 $self->{flattening} = 1.0 / 299.1528128; # Bessel 1841.
262 0         0 $self->{central_meridian} = undef;
263 0         0 $self->{lat_of_origin} = 0.0;
264 0         0 $self->{scale} = 1.0;
265 0         0 $self->{false_northing} = 0.0;
266 0         0 $self->{false_easting} = 1500000.0;
267             }
268              
269             sub _sweref99_params() {
270 0     0   0 my $self = shift;
271 0         0 $self->{axis} = 6378137.0; # GRS 80.
272 0         0 $self->{flattening} = 1.0 / 298.257222101; # GRS 80.
273 0         0 $self->{central_meridian} = undef;
274 0         0 $self->{lat_of_origin} = 0.0;
275 0         0 $self->{scale} = 1.0;
276 0         0 $self->{false_northing} = 0.0;
277 0         0 $self->{false_easting} = 150000.0;
278             }
279              
280              
281             =item ($x, $y) = $grid->geodetic_to_grid($lat, $lon)
282              
283             Conversion from geodetic coordinates to grid coordinates.
284              
285             Parameters:
286              
287             $latitude (number)
288             $longitude (number)
289              
290             Example:
291              
292             my ($x, $y) = $grid->geodetic_to_grid(63.1530261140462, 11.8353976399345);
293              
294             Returns:
295              
296             the x and y grid coordinates;
297              
298             =cut
299              
300             sub geodetic_to_grid {
301 1     1 1 433 my $self = shift;
302 1         2 my ($latitude, $longitude) = @_;
303              
304 1 50       5 unless (defined $self->{central_meridian}) {
305 0         0 return (undef, undef);
306             }
307              
308             # Prepare ellipsoid-based stuff.
309 1         5 my $e2 = $self->{flattening} * (2.0 - $self->{flattening});
310 1         3 my $n = $self->{flattening} / (2.0 - $self->{flattening});
311 1         6 my $a_roof = $self->{axis} / (1.0 + $n) * (1.0 + $n*$n/4.0 + $n*$n*$n*$n/64.0);
312 1         2 my $A = $e2;
313 1         512 my $B = (5.0*$e2*$e2 - $e2*$e2*$e2) / 6.0;
314 1         17 my $C = (104.0*$e2*$e2*$e2 - 45.0*$e2*$e2*$e2*$e2) / 120.0;
315 1         3 my $D = (1237.0*$e2*$e2*$e2*$e2) / 1260.0;
316 1         6 my $beta1 = $n/2.0 - 2.0*$n*$n/3.0 + 5.0*$n*$n*$n/16.0 + 41.0*$n*$n*$n*$n/180.0;
317 1         4 my $beta2 = 13.0*$n*$n/48.0 - 3.0*$n*$n*$n/5.0 + 557.0*$n*$n*$n*$n/1440.0;
318 1         3 my $beta3 = 61.0*$n*$n*$n/240.0 - 103.0*$n*$n*$n*$n/140.0;
319 1         227 my $beta4 = 49561.0*$n*$n*$n*$n/161280.0;
320              
321             # Convert.
322 1         3 my $deg_to_rad = pi / 180.0;
323 1         3 my $phi = $latitude * $deg_to_rad;
324 1         1 my $lambda = $longitude * $deg_to_rad;
325 1         3 my $lambda_zero = $self->{central_meridian} * $deg_to_rad;
326              
327 1         16 my $phi_star = $phi - sin($phi) * cos($phi) * ($A +
328             $B * sin($phi) ** 2 +
329             $C * sin($phi) ** 4 +
330             $D * sin($phi) ** 6);
331 1         3 my $delta_lambda = $lambda - $lambda_zero;
332 1         7 my $xi_prim = atan(tan($phi_star) / cos($delta_lambda));
333 1         83 my $eta_prim = atanh(cos($phi_star) * sin($delta_lambda));
334 1         16 my $x = $self->{scale} * $a_roof * ($xi_prim +
335             $beta1 * sin(2.0*$xi_prim) * cosh(2.0*$eta_prim) +
336             $beta2 * sin(4.0*$xi_prim) * cosh(4.0*$eta_prim) +
337             $beta3 * sin(6.0*$xi_prim) * cosh(6.0*$eta_prim) +
338             $beta4 * sin(8.0*$xi_prim) * cosh(8.0*$eta_prim)) +
339             $self->{false_northing};
340 1         36 my $y = $self->{scale} * $a_roof * ($eta_prim +
341             $beta1 * cos(2.0*$xi_prim) * sinh(2.0*$eta_prim) +
342             $beta2 * cos(4.0*$xi_prim) * sinh(4.0*$eta_prim) +
343             $beta3 * cos(6.0*$xi_prim) * sinh(6.0*$eta_prim) +
344             $beta4 * cos(8.0*$xi_prim) * sinh(8.0*$eta_prim)) +
345             $self->{false_easting};
346 1         43 $x = int($x * 1000.0) / 1000.0;
347 1         2 $y = int($y * 1000.0) / 1000.0;
348 1         6 return ($x, $y);
349             }
350              
351              
352             =item ($lat, $lon) = $grid->grid_to_geodetic($x, $y)
353              
354             Conversion from grid coordinates to geodetic coordinates.
355              
356             Parameters:
357              
358             $x (number)
359             $y (number)
360              
361             Example:
362              
363             my ($lat, $lon) = $grid->grid_to_geodetic(7011002, 1299996);
364              
365             Returns:
366              
367             the latitude and longitude geodetic coordinates;
368              
369             =back
370              
371             =cut
372              
373             sub grid_to_geodetic {
374 1     1 1 528 my $self = shift;
375 1         3 my ($x, $y) = @_;
376              
377 1 50       5 unless (defined $self->{central_meridian}) {
378 0         0 return (undef, undef);
379             }
380              
381             # Prepare ellipsoid-based stuff.
382 1         4 my $e2 = $self->{flattening} * (2.0 - $self->{flattening});
383 1         4 my $n = $self->{flattening} / (2.0 - $self->{flattening});
384 1         6 my $a_roof = $self->{axis} / (1.0 + $n) * (1.0 + $n*$n/4.0 + $n*$n*$n*$n/64.0);
385 1         10 my $delta1 = $n/2.0 - 2.0*$n*$n/3.0 + 37.0*$n*$n*$n/96.0 - $n*$n*$n*$n/360.0;
386 1         4 my $delta2 = $n*$n/48.0 + $n*$n*$n/15.0 - 437.0*$n*$n*$n*$n/1440.0;
387 1         5 my $delta3 = 17.0*$n*$n*$n/480.0 - 37*$n*$n*$n*$n/840.0;
388 1         4 my $delta4 = 4397.0*$n*$n*$n*$n/161280.0;
389              
390 1         3 my $Astar = $e2 + $e2*$e2 + $e2*$e2*$e2 + $e2*$e2*$e2*$e2;
391 1         5 my $Bstar = -(7.0*$e2*$e2 + 17.0*$e2*$e2*$e2 + 30.0*$e2*$e2*$e2*$e2) / 6.0;
392 1         26 my $Cstar = (224.0*$e2*$e2*$e2 + 889.0*$e2*$e2*$e2*$e2) / 120.0;
393 1         9 my $Dstar = -(4279.0*$e2*$e2*$e2*$e2) / 1260.0;
394              
395             # Convert.
396 1         2 my $deg_to_rad = pi / 180;
397 1         3 my $lambda_zero = $self->{central_meridian} * $deg_to_rad;
398 1         3 my $xi = ($x - $self->{false_northing}) / ($self->{scale} * $a_roof);
399 1         4 my $eta = ($y - $self->{false_easting}) / ($self->{scale} * $a_roof);
400 1         24 my $xi_prim = $xi -
401             $delta1*sin(2.0*$xi) * cosh(2.0*$eta) -
402             $delta2*sin(4.0*$xi) * cosh(4.0*$eta) -
403             $delta3*sin(6.0*$xi) * cosh(6.0*$eta) -
404             $delta4*sin(8.0*$xi) * cosh(8.0*$eta);
405 1         68 my $eta_prim = $eta -
406             $delta1*cos(2.0*$xi) * sinh(2.0*$eta) -
407             $delta2*cos(4.0*$xi) * sinh(4.0*$eta) -
408             $delta3*cos(6.0*$xi) * sinh(6.0*$eta) -
409             $delta4*cos(8.0*$xi) * sinh(8.0*$eta);
410 1         37 my $phi_star = asin(sin($xi_prim) / cosh($eta_prim));
411 1         30 my $delta_lambda = atan(sinh($eta_prim) / cos($xi_prim));
412 1         23 my $lon_radian = $lambda_zero + $delta_lambda;
413 1         18 my $lat_radian = $phi_star + sin($phi_star) * cos($phi_star) *
414             ($Astar +
415             $Bstar*sin($phi_star) ** 2 +
416             $Cstar*sin($phi_star) ** 4 +
417             $Dstar*sin($phi_star) ** 6);
418              
419 1         3 my $latitude = $lat_radian * 180.0 / pi;
420 1         7 my $longitude = $lon_radian * 180.0 / pi;
421 1         5 return ($latitude, $longitude);
422             }
423              
424             1;
425              
426             =head1 SEE ALSO
427              
428             Source: http://www.lantmateriet.se/templates/LMV_Entrance.aspx?id=68&lang=EN
429              
430             =head1 COPYRIGHT
431              
432             License: http://creativecommons.org/licenses/by-nc-sa/3.0/
433              
434             =head1 AUTHORS
435              
436             Original Javascript-version: http://mellifica.se/geodesi/gausskruger.js
437              
438             Author: Arnold Andreasson, 2007. http://mellifica.se/konsult
439              
440             Rewritten as object-oriented Perl by: Johan Beronius, 2009. http://www.athega.se/
441              
442             =cut