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   71695 use strict;
  6         43  
  6         178  
4 6     6   32 use warnings;
  6         9  
  6         253  
5              
6             our $VERSION = '1.05';
7              
8 6     6   31 use Carp qw(croak);
  6         13  
  6         529  
9 6     6   43 use Exporter qw(import);
  6         11  
  6         756  
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         11758 DEG2RAD => atan2(1, 1) / 45,
22             WMM_RELEASE_YEAR => 2020,
23             WMM_EXPIRE_YEAR => 2024,
24             DEFAULT_ALT_ARG => 0,
25 6     6   42 };
  6         13  
26              
27             *mag_var = \&mag_dec;
28              
29             sub mag_dec {
30 15     15 1 7863 my ($X, $Y) = mag_field(_args(@_));
31 15         306 return atan2($Y, $X) / DEG2RAD;
32             }
33             sub mag_inc {
34 5     5 1 2726 my ($X, $Y, $Z) = mag_field(_args(@_));
35 5         142 return atan2($Z, sqrt($X*$X + $Y*$Y)) / DEG2RAD;
36             }
37             sub mag_field {
38 20     20 1 57 my ($lat, $lon, $hgt, $yr) = @_;
39              
40 20         39 $lon *= DEG2RAD;
41 20         31 $lat *= DEG2RAD;
42              
43 20         102 my @WMM = _wmm();
44              
45 20         48 my ($geo_r, $geo_lat) = do {
46             # geocentric coordinates
47 20         35 my $A = 6378137;
48              
49             # reference ellipsoid semimajor axis
50 20         34 my $f = 1 / 298.257223563;
51              
52             # flattening
53 20         45 my $e2 = $f * (2 - $f);
54              
55             # eccentricity
56 20         153 my $Rc = $A / sqrt(1 - $e2 * sin($lat) ** 2);
57              
58             # radius of curvature
59 20         109 my $p = ($Rc + $hgt) * cos($lat);
60              
61             # radius in x-y plane
62 20         47 my $z = ($Rc * (1 - $e2) + $hgt) * sin($lat);
63 20         84 (sqrt($p * $p + $z * $z), atan2($z, $p))
64             };
65 20         36 my $s = sin($geo_lat);
66 20         32 my $c = cos($geo_lat);
67              
68             # associated Legendre polynomials (P) and derivatives (dP)
69 20         55 my @P = ([ 1 ], [ $s, $c ]);
70 20         52 my @dP = ([ 0 ], [ $c, - $s ]);
71 20         62 for my $n (2 .. $#WMM) {
72 220         352 my $k = 2 * $n - 1;
73 220         365 for my $m (0 .. $n - 2) {
74 1320         1826 my $k1 = $k / ($n - $m);
75 1320         1918 my $k2 = ($n + $m - 1) / ($n - $m);
76 1320         2422 $P[$n][$m] = $k1 * $s * $P[$n - 1][$m] - $k2 * $P[$n - 2][$m];
77 1320         2780 $dP[$n][$m] = $k1 * ($s * $dP[$n - 1][$m] + $c * $P[$n - 1][$m])
78             - $k2 * $dP[$n - 2][$m];
79             }
80 220         347 my $y = $k * $P[$n - 1][$n - 1];
81 220         368 my $dy = $k * $dP[$n - 1][$n - 1];
82 220         384 $P[$n][$n - 1] = $s * $y;
83 220         402 $dP[$n][$n - 1] = $s * $dy + $c * $y;
84 220         353 $P[$n][$n] = $c * $y;
85 220         405 $dP[$n][$n] = $c * $dy - $s * $y;
86             }
87              
88             # Schmidt quasi-normalization
89 20         49 for my $n (1 .. $#WMM) {
90 240         327 my $f = sqrt(2);
91 240         357 for my $m (1 .. $n) {
92 1560         2253 $f /= sqrt(($n - $m + 1) * ($n + $m));
93 1560         2087 $P[$n][$m] *= $f;
94 1560         2140 $dP[$n][$m] *= $f;
95             }
96             }
97              
98 20         35 my $X = 0; # magnetic field north component in nT
99 20         34 my $Y = 0; # east component
100 20         30 my $Z = 0; # vertical component
101 20         34 my $t = $yr - WMM_RELEASE_YEAR;
102 20         34 my $r = 6371200 / $geo_r; # radius relative to geomagnetic reference
103 20         33 my $R = $r * $r;
104 20         209 my @c = map cos($_ * $lon), 0 .. $#WMM;
105 20         155 my @s = map sin($_ * $lon), 0 .. $#WMM;
106 20         56 for my $n (1 .. $#WMM) {
107 240         358 my $x = my $y = my $z = 0;
108 240         364 for my $m (0 .. $n) {
109 1800         2394 my $row = $WMM[$n][$m];
110 1800         2886 my $g = $row->[0] + $t * $row->[2];
111 1800         2883 my $h = $row->[1] + $t * $row->[3];
112 1800         2949 $x += ($g * $c[$m] + $h * $s[$m]) * $dP[$n][$m];
113 1800         3035 $y += ($g * $s[$m] - $h * $c[$m]) * $P[$n][$m] * $m;
114 1800         3219 $z += ($g * $c[$m] + $h * $s[$m]) * $P[$n][$m];
115             }
116 240         314 $R *= $r;
117 240         327 $X -= $x * $R;
118 240         323 $Y += $y * $R;
119 240         394 $Z -= $z * $R * ($n + 1);
120             }
121 20         48 $Y /= $c;
122              
123 20         47 $c = cos($geo_lat - $lat); # transform back to geodetic coords
124 20         32 $s = sin($geo_lat - $lat);
125 20         58 ($X, $Z) = ($X * $c - $Z * $s, $X * $s + $Z * $c);
126              
127 20         429 return ($X, $Y, $Z);
128             }
129             sub _args {
130 41 100   41   18340 croak("Minimum latitude and longitude must be sent in\n") if @_ < 2;
131              
132 39         110 my ($lat, $lon, $alt, $year) = @_;
133              
134 39 100       590 croak("Latitude must be a number\n") if $lat !~ /^-?\d+(?:\.\d+)?$/;
135 36 100       428 croak("Longitude must be a number\n") if $lon !~ /^-?\d+(?:\.\d+)?$/;
136 33 100 100     562 croak("Altitude must be an integer\n") if defined $alt && $alt !~ /^\d+$/;
137 29 100 100     457 croak("Year must be a number\n")
138             if defined $year && $year !~ /^-?\d+(?:\.\d+)?$/;
139              
140 26 100 100     124 if ($lat < -180 || $lat > 180){
141 2         169 croak("Latitude must be between -180 and 180 degrees\n");
142             }
143 24 100 100     90 if ($lon < -180 || $lon > 180){
144 2         162 croak("Longitude must be between -180 and 180 degrees\n");
145             }
146              
147 22 100       61 $alt = defined $alt ? $alt : DEFAULT_ALT_ARG;
148 22 100       50 $year = defined $year ? $year : _calc_year();
149              
150 22 100 100     87 if ($year < WMM_RELEASE_YEAR || $year > WMM_EXPIRE_YEAR){
151 2         14 warn "Calculation model is expired: "
152             . WMM_RELEASE_YEAR . '-' . WMM_EXPIRE_YEAR . "\n";
153             }
154              
155 22         82 return ($lat, $lon, $alt, $year);
156             }
157             sub _calc_year {
158 2     2   101 my (undef, undef, undef, undef, $month_num, $year) = localtime;
159 2         10 $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         8 my $month_normalized = int(((($month_num - 1) * (10 - 1)) / (12 - 1)) + 1);
171              
172 2         24 return "$year.$month_normalized";
173             }
174             sub _wmm {
175             # 2015 v2
176 20     20   1161 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         134 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             =for html
303            
304             Coverage Status
305              
306             =head1 SYNOPSIS
307              
308             use Geo::Compass::Variation qw(mag_dec mag_inc);
309              
310             my $lat = 53.1234567;
311             my $lon = -114.1234567;
312             my $alt = 1098;
313              
314             my $declination = mag_dec($lat, $lon, $alt);
315             my $inclination = mag_inc($lat, $lon, $alt);
316              
317             =head1 DESCRIPTION
318              
319             This module calculates and returns the Magnetic declination and inclination
320             (dip) calculations based on WMM earth magnetism model for a specified latitude
321             and longitude pair.
322              
323             See L for details.
324              
325             The WMM data is currently valid from January 1, 2020 through December 31, 2024.
326             This module will be updated with new WMM data as it becomes available. (Last
327             update was the Dec 10, 2019 dataset).
328              
329             =head1 EXPORT_OK
330              
331             All functions must be imported explicitly:
332              
333             use Geo::Compass::Variation qw(mag_dec mag_inc);
334              
335             # or
336              
337             use Geo::Compass::Variation qw(:all);
338              
339             Note: The C function has an alias of C which can be imported
340             explicitly, or with the C<:all> tag.
341              
342             =head1 FUNCTIONS
343              
344             =head2 mag_dec
345              
346             Calculates and returns the magnetic declination of a pair of GPS coordinates.
347              
348             Parameters:
349              
350             $lat
351              
352             Mandatory, Float: Latitude, in signed notation (eg: C<53.1111111>. Negative is
353             South and positive is North of the Equator.
354              
355             $lon
356              
357             Mandatory, Float: Longitude, in signed notiation (eg: C<-114.11111>. Negative is
358             West and positive is East of the Prime Meridian.
359              
360             $alt
361              
362             Optional, Integer: Altitude above sea level, in metres. Defaults to C<0>.
363              
364             $year
365              
366             Optional, Integer|Float: The year to base the calculation from. Defaults to
367             C, where C is the year from C and C is the month
368             number from C, normalized to a digit between 1-10.
369              
370             We will C if the year is out of range of the current WMM specification.
371              
372             Return: A floating point number representing the magnetic declination.
373              
374             =head2 mag_var
375              
376             Simply an alias for L.
377              
378             =head2 mag_inc
379              
380             Calculates and returns the magnetic inclination of a pair of GPS coordinates.
381              
382             Parameters:
383              
384             Parameters are exactly the same as for the L function. Please review
385             that documentation section for full details.
386              
387             Return: A floating point number representing the magnetic inclination.
388              
389             =head2 mag_field
390              
391             Core function that calcluates the raw magnetic field north component (C<$X>),
392             the east component (C<$Y>) and the vertical component (C<$Z>).
393              
394             Takes the same parameters as L. Please see that function's
395             documentation for full details.
396              
397             =head1 AUTHOR
398              
399             Steve Bertrand, C<< >>
400              
401             =head1 ACKNOWLEDGEMENTS
402              
403             All the thanks goes out to L of
404             L for all of the core functionality.
405              
406             It was presented L, in response to
407             L I had started regarding a
408             code review of some prototype code I wrote to calculate the direction between
409             two pairs of GPS coordinates.
410              
411             =head1 LICENSE AND COPYRIGHT
412              
413             Copyright 2017-2020 Steve Bertrand.
414              
415             This program is free software; you can redistribute it and/or modify it under
416             the terms of either: the GNU General Public License as published by the Free
417             Software Foundation; or the Artistic License.
418              
419             See L for more information.
420