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   58021 use strict;
  6         34  
  6         144  
4 6     6   25 use warnings;
  6         12  
  6         243  
5              
6             our $VERSION = '1.06';
7              
8 6     6   28 use Carp qw(croak);
  6         8  
  6         461  
9 6     6   35 use Exporter qw(import);
  6         9  
  6         783  
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         9916 DEG2RAD => atan2(1, 1) / 45,
22             WMM_RELEASE_YEAR => 2020,
23             WMM_EXPIRE_YEAR => 2024,
24             DEFAULT_ALT_ARG => 0,
25 6     6   37 };
  6         10  
26              
27             *mag_var = \&mag_dec;
28              
29             sub mag_dec {
30 15     15 1 7432 my ($X, $Y) = mag_field(_args(@_));
31 15         280 return atan2($Y, $X) / DEG2RAD;
32             }
33             sub mag_inc {
34 5     5 1 3294 my ($X, $Y, $Z) = mag_field(_args(@_));
35 5         124 return atan2($Z, sqrt($X*$X + $Y*$Y)) / DEG2RAD;
36             }
37             sub mag_field {
38 20     20 1 43 my ($lat, $lon, $hgt, $yr) = @_;
39              
40 20         34 $lon *= DEG2RAD;
41 20         24 $lat *= DEG2RAD;
42              
43 20         86 my @WMM = _wmm();
44              
45 20         39 my ($geo_r, $geo_lat) = do {
46             # geocentric coordinates
47 20         30 my $A = 6378137;
48              
49             # reference ellipsoid semimajor axis
50 20         26 my $f = 1 / 298.257223563;
51              
52             # flattening
53 20         38 my $e2 = $f * (2 - $f);
54              
55             # eccentricity
56 20         127 my $Rc = $A / sqrt(1 - $e2 * sin($lat) ** 2);
57              
58             # radius of curvature
59 20         90 my $p = ($Rc + $hgt) * cos($lat);
60              
61             # radius in x-y plane
62 20         41 my $z = ($Rc * (1 - $e2) + $hgt) * sin($lat);
63 20         69 (sqrt($p * $p + $z * $z), atan2($z, $p))
64             };
65 20         31 my $s = sin($geo_lat);
66 20         29 my $c = cos($geo_lat);
67              
68             # associated Legendre polynomials (P) and derivatives (dP)
69 20         44 my @P = ([ 1 ], [ $s, $c ]);
70 20         44 my @dP = ([ 0 ], [ $c, - $s ]);
71 20         50 for my $n (2 .. $#WMM) {
72 220         267 my $k = 2 * $n - 1;
73 220         280 for my $m (0 .. $n - 2) {
74 1320         1574 my $k1 = $k / ($n - $m);
75 1320         2087 my $k2 = ($n + $m - 1) / ($n - $m);
76 1320         2015 $P[$n][$m] = $k1 * $s * $P[$n - 1][$m] - $k2 * $P[$n - 2][$m];
77 1320         2411 $dP[$n][$m] = $k1 * ($s * $dP[$n - 1][$m] + $c * $P[$n - 1][$m])
78             - $k2 * $dP[$n - 2][$m];
79             }
80 220         292 my $y = $k * $P[$n - 1][$n - 1];
81 220         296 my $dy = $k * $dP[$n - 1][$n - 1];
82 220         302 $P[$n][$n - 1] = $s * $y;
83 220         434 $dP[$n][$n - 1] = $s * $dy + $c * $y;
84 220         454 $P[$n][$n] = $c * $y;
85 220         364 $dP[$n][$n] = $c * $dy - $s * $y;
86             }
87              
88             # Schmidt quasi-normalization
89 20         43 for my $n (1 .. $#WMM) {
90 240         255 my $f = sqrt(2);
91 240         285 for my $m (1 .. $n) {
92 1560         1820 $f /= sqrt(($n - $m + 1) * ($n + $m));
93 1560         1632 $P[$n][$m] *= $f;
94 1560         1798 $dP[$n][$m] *= $f;
95             }
96             }
97              
98 20         25 my $X = 0; # magnetic field north component in nT
99 20         32 my $Y = 0; # east component
100 20         29 my $Z = 0; # vertical component
101 20         25 my $t = $yr - WMM_RELEASE_YEAR;
102 20         30 my $r = 6371200 / $geo_r; # radius relative to geomagnetic reference
103 20         26 my $R = $r * $r;
104 20         181 my @c = map cos($_ * $lon), 0 .. $#WMM;
105 20         128 my @s = map sin($_ * $lon), 0 .. $#WMM;
106 20         60 for my $n (1 .. $#WMM) {
107 240         301 my $x = my $y = my $z = 0;
108 240         301 for my $m (0 .. $n) {
109 1800         2086 my $row = $WMM[$n][$m];
110 1800         2351 my $g = $row->[0] + $t * $row->[2];
111 1800         2307 my $h = $row->[1] + $t * $row->[3];
112 1800         2309 $x += ($g * $c[$m] + $h * $s[$m]) * $dP[$n][$m];
113 1800         2362 $y += ($g * $s[$m] - $h * $c[$m]) * $P[$n][$m] * $m;
114 1800         2527 $z += ($g * $c[$m] + $h * $s[$m]) * $P[$n][$m];
115             }
116 240         248 $R *= $r;
117 240         264 $X -= $x * $R;
118 240         263 $Y += $y * $R;
119 240         327 $Z -= $z * $R * ($n + 1);
120             }
121 20         28 $Y /= $c;
122              
123 20         37 $c = cos($geo_lat - $lat); # transform back to geodetic coords
124 20         29 $s = sin($geo_lat - $lat);
125 20         41 ($X, $Z) = ($X * $c - $Z * $s, $X * $s + $Z * $c);
126              
127 20         347 return ($X, $Y, $Z);
128             }
129             sub _args {
130 41 100   41   24553 croak("Minimum latitude and longitude must be sent in\n") if @_ < 2;
131              
132 39         87 my ($lat, $lon, $alt, $year) = @_;
133              
134 39 100       689 croak("Latitude must be a number\n") if $lat !~ /^-?\d+(?:\.\d+)?$/;
135 36 100       362 croak("Longitude must be a number\n") if $lon !~ /^-?\d+(?:\.\d+)?$/;
136 33 100 100     754 croak("Altitude must be an integer\n") if defined $alt && $alt !~ /^\d+$/;
137 29 100 100     373 croak("Year must be a number\n")
138             if defined $year && $year !~ /^-?\d+(?:\.\d+)?$/;
139              
140 26 100 100     105 if ($lat < -180 || $lat > 180){
141 2         147 croak("Latitude must be between -180 and 180 degrees\n");
142             }
143 24 100 100     80 if ($lon < -180 || $lon > 180){
144 2         196 croak("Longitude must be between -180 and 180 degrees\n");
145             }
146              
147 22 100       51 $alt = defined $alt ? $alt : DEFAULT_ALT_ARG;
148 22 100       41 $year = defined $year ? $year : _calc_year();
149              
150 22 100 100     78 if ($year < WMM_RELEASE_YEAR || $year > WMM_EXPIRE_YEAR){
151 2         11 warn "Calculation model is expired: "
152             . WMM_RELEASE_YEAR . '-' . WMM_EXPIRE_YEAR . "\n";
153             }
154              
155 22         70 return ($lat, $lon, $alt, $year);
156             }
157             sub _calc_year {
158 2     2   90 my (undef, undef, undef, undef, $month_num, $year) = localtime;
159 2         8 $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         6 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   952 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         113 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