File Coverage

blib/lib/Geo/Compass/Variation.pm
Criterion Covered Total %
statement 102 102 100.0
branch 20 20 100.0
condition 15 15 100.0
subroutine 11 12 91.6
pod 3 3 100.0
total 151 152 99.3


line stmt bran cond sub pod time code
1             package Geo::Compass::Variation;
2              
3 6     6   70889 use strict;
  6         42  
  6         170  
4 6     6   31 use warnings;
  6         12  
  6         251  
5              
6             our $VERSION = '1.04';
7              
8 6     6   30 use Carp qw(croak);
  6         12  
  6         577  
9 6     6   45 use Exporter qw(import);
  6         11  
  6         799  
10            
11             our @EXPORT_OK = qw(
12             mag_dec
13             mag_var
14             mag_inc
15             );
16            
17             our %EXPORT_TAGS;
18             $EXPORT_TAGS{all} = [@EXPORT_OK];
19              
20             use constant {
21 6         12297 DEG2RAD => atan2(1, 1) / 45,
22             WMM_RELEASE_YEAR => 2020,
23             WMM_EXPIRE_YEAR => 2024,
24             DEFAULT_ALT_ARG => 0,
25 6     6   43 };
  6         11  
26              
27             *mag_var = \&mag_dec;
28              
29             sub mag_dec {
30 15     15 1 7827 my ($X, $Y) = mag_field(_args(@_));
31 15         313 return atan2($Y, $X) / DEG2RAD;
32             }
33             sub mag_inc {
34 5     5 1 2917 my ($X, $Y, $Z) = mag_field(_args(@_));
35 5         157 return atan2($Z, sqrt($X*$X + $Y*$Y)) / DEG2RAD;
36             }
37             sub mag_field {
38 20     20 1 54 my ($lat, $lon, $hgt, $yr) = @_;
39              
40 20         42 $lon *= DEG2RAD;
41 20         30 $lat *= DEG2RAD;
42              
43 20         97 my @WMM = _wmm();
44              
45 20         57 my ($geo_r, $geo_lat) = do {
46             # geocentric coordinates
47 20         37 my $A = 6378137;
48              
49             # reference ellipsoid semimajor axis
50 20         32 my $f = 1 / 298.257223563;
51              
52             # flattening
53 20         42 my $e2 = $f * (2 - $f);
54              
55             # eccentricity
56 20         160 my $Rc = $A / sqrt(1 - $e2 * sin($lat) ** 2);
57              
58             # radius of curvature
59 20         74 my $p = ($Rc + $hgt) * cos($lat);
60              
61             # radius in x-y plane
62 20         43 my $z = ($Rc * (1 - $e2) + $hgt) * sin($lat);
63 20         93 (sqrt($p * $p + $z * $z), atan2($z, $p))
64             };
65 20         37 my $s = sin($geo_lat);
66 20         34 my $c = cos($geo_lat);
67              
68             # associated Legendre polynomials (P) and derivatives (dP)
69 20         56 my @P = ([ 1 ], [ $s, $c ]);
70 20         53 my @dP = ([ 0 ], [ $c, - $s ]);
71 20         60 for my $n (2 .. $#WMM) {
72 220         322 my $k = 2 * $n - 1;
73 220         355 for my $m (0 .. $n - 2) {
74 1320         1838 my $k1 = $k / ($n - $m);
75 1320         1950 my $k2 = ($n + $m - 1) / ($n - $m);
76 1320         2418 $P[$n][$m] = $k1 * $s * $P[$n - 1][$m] - $k2 * $P[$n - 2][$m];
77 1320         2775 $dP[$n][$m] = $k1 * ($s * $dP[$n - 1][$m] + $c * $P[$n - 1][$m])
78             - $k2 * $dP[$n - 2][$m];
79             }
80 220         334 my $y = $k * $P[$n - 1][$n - 1];
81 220         323 my $dy = $k * $dP[$n - 1][$n - 1];
82 220         414 $P[$n][$n - 1] = $s * $y;
83 220         382 $dP[$n][$n - 1] = $s * $dy + $c * $y;
84 220         359 $P[$n][$n] = $c * $y;
85 220         446 $dP[$n][$n] = $c * $dy - $s * $y;
86             }
87              
88             # Schmidt quasi-normalization
89 20         55 for my $n (1 .. $#WMM) {
90 240         308 my $f = sqrt(2);
91 240         350 for my $m (1 .. $n) {
92 1560         2274 $f /= sqrt(($n - $m + 1) * ($n + $m));
93 1560         2012 $P[$n][$m] *= $f;
94 1560         2120 $dP[$n][$m] *= $f;
95             }
96             }
97              
98 20         34 my $X = 0; # magnetic field north component in nT
99 20         30 my $Y = 0; # east component
100 20         27 my $Z = 0; # vertical component
101 20         41 my $t = $yr - WMM_RELEASE_YEAR;
102 20         34 my $r = 6371200 / $geo_r; # radius relative to geomagnetic reference
103 20         34 my $R = $r * $r;
104 20         212 my @c = map cos($_ * $lon), 0 .. $#WMM;
105 20         153 my @s = map sin($_ * $lon), 0 .. $#WMM;
106 20         58 for my $n (1 .. $#WMM) {
107 240         349 my $x = my $y = my $z = 0;
108 240         385 for my $m (0 .. $n) {
109 1800         2581 my $row = $WMM[$n][$m];
110 1800         2929 my $g = $row->[0] + $t * $row->[2];
111 1800         2853 my $h = $row->[1] + $t * $row->[3];
112 1800         2767 $x += ($g * $c[$m] + $h * $s[$m]) * $dP[$n][$m];
113 1800         3016 $y += ($g * $s[$m] - $h * $c[$m]) * $P[$n][$m] * $m;
114 1800         3133 $z += ($g * $c[$m] + $h * $s[$m]) * $P[$n][$m];
115             }
116 240         324 $R *= $r;
117 240         315 $X -= $x * $R;
118 240         313 $Y += $y * $R;
119 240         402 $Z -= $z * $R * ($n + 1);
120             }
121 20         37 $Y /= $c;
122              
123 20         40 $c = cos($geo_lat - $lat); # transform back to geodetic coords
124 20         35 $s = sin($geo_lat - $lat);
125 20         55 ($X, $Z) = ($X * $c - $Z * $s, $X * $s + $Z * $c);
126              
127 20         483 return ($X, $Y, $Z);
128             }
129             sub _args {
130 41 100   41   18824 croak("Minimum latitude and longitude must be sent in\n") if @_ < 2;
131              
132 39         103 my ($lat, $lon, $alt, $year) = @_;
133              
134 39 100       598 croak("Latitude must be a number\n") if $lat !~ /^-?\d+(?:\.\d+)?$/;
135 36 100       429 croak("Longitude must be a number\n") if $lon !~ /^-?\d+(?:\.\d+)?$/;
136 33 100 100     581 croak("Altitude must be an integer\n") if defined $alt && $alt !~ /^\d+$/;
137 29 100 100     450 croak("Year must be a number\n")
138             if defined $year && $year !~ /^-?\d+(?:\.\d+)?$/;
139              
140 26 100 100     131 if ($lat < -180 || $lat > 180){
141 2         168 croak("Latitude must be between -180 and 180 degrees\n");
142             }
143 24 100 100     111 if ($lon < -180 || $lon > 180){
144 2         169 croak("Longitude must be between -180 and 180 degrees\n");
145             }
146              
147 22 100       59 $alt = defined $alt ? $alt : DEFAULT_ALT_ARG;
148 22 100       51 $year = defined $year ? $year : _calc_year();
149              
150 22 100 100     97 if ($year < WMM_RELEASE_YEAR || $year > WMM_EXPIRE_YEAR){
151 2         15 warn "Calculation model is expired: "
152             . WMM_RELEASE_YEAR . '-' . WMM_EXPIRE_YEAR . "\n";
153             }
154              
155 22         79 return ($lat, $lon, $alt, $year);
156             }
157             sub _calc_year {
158 2     2   93 my (undef, undef, undef, undef, $month_num, $year) = localtime;
159 2         9 $year += 1900;
160 2         4 $month_num += 1; # starts at zero
161              
162             # normalization of month, where:
163             # oldvalue = $month_num
164             # oldmin = 1 (month starts at one)
165             # newmax = 10
166             # newmin = 1
167             # oldmax = 12
168              
169             # ((oldvalue - oldmin) * (newmax - newmin)) / (oldmax - oldmin) + newmin
170 2         11 my $month_normalized = int(((($month_num - 1) * (10 - 1)) / (12 - 1)) + 1);
171              
172 2         25 return "$year.$month_normalized";
173             }
174             sub _wmm {
175             # 2015 v2
176 20     20   1118 my $wmm = [
177             [],
178             [[-29404.5, "0.0", 6.7, "0.0"], [-1450.7, 4652.9, 7.7, -25.1]],
179             [
180             ["-2500.0", "0.0", -11.5, "0.0"],
181             ["2982.0", -2991.6, -7.1, -30.2],
182             [1676.8, -734.8, -2.2, -23.9],
183             ],
184             [
185             [1363.9, "0.0", 2.8, "0.0"],
186             ["-2381.0", -82.2, -6.2, 5.7],
187             [1236.2, 241.8, 3.4, "-1.0"],
188             [525.7, -542.9, -12.2, 1.1],
189             ],
190             [
191             [903.1, "0.0", -1.1, "0.0"],
192             [809.4, "282.0", -1.6, 0.2],
193             [86.2, -158.4, "-6.0", 6.9],
194             [-309.4, 199.8, 5.4, 3.7],
195             [47.9, -350.1, -5.5, -5.6],
196             ],
197             [
198             [-234.4, "0.0", -0.3, "0.0"],
199             [363.1, 47.7, 0.6, 0.1],
200             [187.8, 208.4, -0.7, 2.5],
201             [-140.7, -121.3, 0.1, -0.9],
202             [-151.2, 32.2, 1.2, "3.0"],
203             [13.7, 99.1, "1.0", 0.5],
204             ],
205             [
206             [65.9, "0.0", -0.6, "0.0"],
207             [65.6, -19.1, -0.4, 0.1],
208             ["73.0", "25.0", 0.5, -1.8],
209             [-121.5, 52.7, 1.4, -1.4],
210             [-36.2, -64.4, -1.4, 0.9],
211             [13.5, "9.0", "-0.0", 0.1],
212             [-64.7, 68.1, 0.8, "1.0"],
213             ],
214             [
215             [80.6, "0.0", -0.1, "0.0"],
216             [-76.8, -51.4, -0.3, 0.5],
217             [-8.3, -16.8, -0.1, 0.6],
218             [56.5, 2.3, 0.7, -0.7],
219             [15.8, 23.5, 0.2, -0.2],
220             [6.4, -2.2, -0.5, -1.2],
221             [-7.2, -27.2, -0.8, 0.2],
222             [9.8, -1.9, "1.0", 0.3],
223             ],
224             [
225             [23.6, "0.0", -0.1, "0.0"],
226             [9.8, 8.4, 0.1, -0.3],
227             [-17.5, -15.3, -0.1, 0.7],
228             [-0.4, 12.8, 0.5, -0.2],
229             [-21.1, -11.8, -0.1, 0.5],
230             [15.3, 14.9, 0.4, -0.3],
231             [13.7, 3.6, 0.5, -0.5],
232             [-16.5, -6.9, "0.0", 0.4],
233             [-0.3, 2.8, 0.4, 0.1],
234             ],
235             [
236             ["5.0", "0.0", -0.1, "0.0"],
237             [8.2, -23.3, -0.2, -0.3],
238             [2.9, 11.1, "-0.0", 0.2],
239             [-1.4, 9.8, 0.4, -0.4],
240             [-1.1, -5.1, -0.3, 0.4],
241             [-13.3, -6.2, "-0.0", 0.1],
242             [1.1, 7.8, 0.3, "-0.0"],
243             [8.9, 0.4, "-0.0", -0.2],
244             [-9.3, -1.5, "-0.0", 0.5],
245             [-11.9, 9.7, -0.4, 0.2],
246             ],
247             [
248             [-1.9, "0.0", "0.0", "0.0"],
249             [-6.2, 3.4, "-0.0", "-0.0"],
250             [-0.1, -0.2, "-0.0", 0.1],
251             [1.7, 3.5, 0.2, -0.3],
252             [-0.9, 4.8, -0.1, 0.1],
253             [0.6, -8.6, -0.2, -0.2],
254             [-0.9, -0.1, "-0.0", 0.1],
255             [1.9, -4.2, -0.1, "-0.0"],
256             [1.4, -3.4, -0.2, -0.1],
257             [-2.4, -0.1, -0.1, 0.2],
258             [-3.9, -8.8, "-0.0", "-0.0"],
259             ],
260             [
261             ["3.0", "0.0", "-0.0", "0.0"],
262             [-1.4, "-0.0", -0.1, "-0.0"],
263             [-2.5, 2.6, "-0.0", 0.1],
264             [2.4, -0.5, "0.0", "0.0"],
265             [-0.9, -0.4, "-0.0", 0.2],
266             [0.3, 0.6, -0.1, "-0.0"],
267             [-0.7, -0.2, "0.0", "0.0"],
268             [-0.1, -1.7, "-0.0", 0.1],
269             [1.4, -1.6, -0.1, "-0.0"],
270             [-0.6, "-3.0", -0.1, -0.1],
271             [0.2, "-2.0", -0.1, "0.0"],
272             [3.1, -2.6, -0.1, "-0.0"],
273             ],
274             [
275             ["-2.0", "0.0", "0.0", "0.0"],
276             [-0.1, -1.2, "-0.0", "-0.0"],
277             [0.5, 0.5, "-0.0", "0.0"],
278             [1.3, 1.3, "0.0", -0.1],
279             [-1.2, -1.8, "-0.0", 0.1],
280             [0.7, 0.1, "-0.0", "-0.0"],
281             [0.3, 0.7, "0.0", "0.0"],
282             [0.5, -0.1, "-0.0", "-0.0"],
283             [-0.2, 0.6, "0.0", 0.1],
284             [-0.5, 0.2, "-0.0", "-0.0"],
285             [0.1, -0.9, "-0.0", "-0.0"],
286             [-1.1, "-0.0", "-0.0", "0.0"],
287             [-0.3, 0.5, -0.1, -0.1],
288             ],
289             ];
290              
291 20         139 return @$wmm;
292             }
293       0     sub _pod_placeholder {}
294              
295             1; __END__
296              
297             =head1 NAME
298              
299             Geo::Compass::Variation - Accurately calculate magnetic declination and
300             inclination
301              
302             =head1 SYNOPSIS
303              
304             use Geo::Compass::Variation qw(mag_dec mag_inc);
305              
306             my $lat = 53.1234567;
307             my $lon = -114.1234567;
308             my $alt = 1098;
309              
310             my $declination = mag_dec($lat, $lon, $alt);
311             my $inclination = mag_inc($lat, $lon, $alt);
312              
313             =head1 DESCRIPTION
314              
315             This module calculates and returns the Magnetic declination and inclination
316             (dip) calculations based on WMM earth magnetism model for a specified latitude
317             and longitude pair.
318              
319             See L for details.
320              
321             The WMM data is currently valid from January 1, 2020 through December 31, 2024.
322             This module will be updated with new WMM data as it becomes available. (Last
323             update was the Dec 10, 2019 dataset).
324              
325             =head1 EXPORT_OK
326              
327             All functions must be imported explicitly:
328              
329             use Geo::Compass::Variation qw(mag_dec mag_inc);
330              
331             # or
332              
333             use Geo::Compass::Variation qw(:all);
334              
335             Note: The C function has an alias of C which can be imported
336             explicitly, or with the C<:all> tag.
337              
338             =head1 FUNCTIONS
339              
340             =head2 mag_dec
341              
342             Calculates and returns the magnetic declination of a pair of GPS coordinates.
343              
344             Parameters:
345              
346             $lat
347              
348             Mandatory, Float: Latitude, in signed notation (eg: C<53.1111111>. Negative is
349             South and positive is North of the Equator.
350              
351             $lon
352              
353             Mandatory, Float: Longitude, in signed notiation (eg: C<-114.11111>. Negative is
354             West and positive is East of the Prime Meridian.
355              
356             $alt
357              
358             Optional, Integer: Altitude above sea level, in metres. Defaults to C<0>.
359              
360             $year
361              
362             Optional, Integer|Float: The year to base the calculation from. Defaults to
363             C, where C is the year from C and C is the month
364             number from C, normalized to a digit between 1-10.
365              
366             We will C if the year is out of range of the current WMM specification.
367              
368             Return: A floating point number representing the magnetic declination.
369              
370             =head2 mag_var
371              
372             Simply an alias for L.
373              
374             =head2 mag_inc
375              
376             Calculates and returns the magnetic inclination of a pair of GPS coordinates.
377              
378             Parameters:
379              
380             Parameters are exactly the same as for the L function. Please review
381             that documentation section for full details.
382              
383             Return: A floating point number representing the magnetic inclination.
384              
385             =head2 mag_field
386              
387             Core function that calcluates the raw magnetic field north component (C<$X>),
388             the east component (C<$Y>) and the vertical component (C<$Z>).
389              
390             Takes the same parameters as L. Please see that function's
391             documentation for full details.
392              
393             =head1 AUTHOR
394              
395             Steve Bertrand, C<< >>
396              
397             =head1 ACKNOWLEDGEMENTS
398              
399             All the thanks goes out to L of
400             L for all of the core functionality.
401              
402             It was presented L, in response to
403             L I had started regarding a
404             code review of some prototype code I wrote to calculate the direction between
405             two pairs of GPS coordinates.
406              
407             =head1 LICENSE AND COPYRIGHT
408              
409             Copyright 2017-2020 Steve Bertrand.
410              
411             This program is free software; you can redistribute it and/or modify it under
412             the terms of either: the GNU General Public License as published by the Free
413             Software Foundation; or the Artistic License.
414              
415             See L for more information.
416