File Coverage

blib/lib/Geo/AnomalyDetector.pm
Criterion Covered Total %
statement 44 44 100.0
branch 11 14 78.5
condition 4 10 40.0
subroutine 6 6 100.0
pod 0 2 0.0
total 65 76 85.5


line stmt bran cond sub pod time code
1             package Geo::AnomalyDetector;
2              
3 1     1   149555 use strict;
  1         2  
  1         28  
4 1     1   4 use warnings;
  1         2  
  1         67  
5              
6 1     1   1562 use Statistics::Basic qw(mean stddev);
  1         14278  
  1         3  
7 1     1   17896 use Math::Trig;
  1         11377  
  1         594  
8             # use Geo::Inverse;
9              
10             =head1 NAME
11              
12             Geo::AnomalyDetector - Detect anomalies in geospatial coordinate datasets
13              
14             =head1 SYNOPSIS
15              
16             This module analyzes latitude and longitude data points to identify anomalies based on their distance from the mean location.
17              
18             use Geo::AnomalyDetector;
19              
20             my $detector = Geo::AnomalyDetector->new(threshold => 3);
21             my $coords = [ [37.7749, -122.4194], [40.7128, -74.0060], [35.6895, 139.6917] ];
22             my $anomalies = $detector->detect_anomalies($coords);
23             print "Anomalies: " . join ", ", map { "($_->[0], $_->[1])" } @{$anomalies};
24              
25             Each co-ordinate can be either a two element array of [latitude, longitude] or an object that has
26             C and C methods.
27              
28             =head1 VERSION
29              
30             0.02
31              
32             =cut
33              
34             our $VERSION = '0.02';
35              
36             sub new {
37 1     1 0 228389 my ($class, %args) = @_;
38             my $self = {
39             threshold => $args{threshold} || 3,
40 1   50     16 unit => $args{unit} || 'K',
      50        
41             };
42 1         3 bless $self, $class;
43 1         4 return $self;
44             }
45              
46             sub detect_anomalies {
47 1     1 0 127 my ($self, $coordinates) = @_;
48              
49 1         3 my @distances;
50 1 100       2 my $mean_lat = mean(map { (ref($_) eq 'ARRAY') ? $_->[0] : $_->latitude() } @{$coordinates});
  4         60  
  1         4  
51 1 100       246 my $mean_lon = mean(map { (ref($_) eq 'ARRAY') ? $_->[1] : $_->longitude() } @{$coordinates});
  4         26  
  1         4  
52              
53             # my $inverse = Geo::Inverse->new();
54              
55 1 50 33     93 die if(!defined($mean_lat) || !defined($mean_lon));
56              
57 1         2 foreach my $coord (@{$coordinates}) {
  1         3  
58 4 100       19 my ($lat, $lon) = (ref($coord) eq 'ARRAY') ? @{$coord} : ($coord->latitude(), $coord->longitude());
  3         7  
59 4 50 33     30 die if(!defined($lat) || !defined($lon));
60             # my $distance = distance($lat, $lon, $mean_lat, $mean_lon, 'K');
61              
62             # Thanks to Robbie Hatley for working out the arguments to Math::Trig
63 4         8 my $t1 = $lon * (pi/180);
64 4         9 my $p1 = (pi/2) - ($lat * (pi/180));
65 4         14 my $t2 = $mean_lon * (pi/180);
66 4         113 my $p2 = (pi/2) - ($mean_lat * (pi/180));
67 4 50       87 my $rho = ($self->{'unit'} eq 'M') ? 3959.0 : 6371.0; # radius of Earth in miles/Km
68 4         16 my $distance = Math::Trig::great_circle_distance($t1, $p1, $t2, $p2, $rho);
69 4         257 push @distances, $distance;
70             }
71              
72 1         5 my $mean_dist = mean(@distances);
73 1         117 my $std_dist = stddev(@distances);
74              
75 1         327 my @anomalies;
76 1         5 for my $i (0 .. $#distances) {
77 4 100       264 if (abs($distances[$i] - $mean_dist) > ($self->{threshold} * $std_dist)) {
78 1         23 push @anomalies, $coordinates->[$i];
79             }
80             }
81              
82 1         34 return \@anomalies;
83             }
84              
85             # Now use Math::Trig. I tried Geo::Inverse, but that always throws messages about undefined variables
86              
87             # =head2 distance
88             #
89             # Calculate the distance between two geographical points using latitude and longitude.
90             # Supports distance in kilometres (K), nautical miles (N), or miles.
91             #
92             # From L
93             # FIXME: use Math::Trig
94             #
95             # =cut
96             #
97             # sub distance {
98             # my ($lat1, $lon1, $lat2, $lon2, $unit) = @_;
99             # my $theta = $lon1 - $lon2;
100             # my $dist = sin(_deg2rad($lat1)) * sin(_deg2rad($lat2)) + cos(_deg2rad($lat1)) * cos(_deg2rad($lat2)) * cos(_deg2rad($theta));
101             # $dist = _acos($dist);
102             # $dist = _rad2deg($dist);
103             # $dist = $dist * 60 * 1.1515;
104             # if ($unit eq 'K') {
105             # $dist = $dist * 1.609344; # number of kilometres in a mile
106             # } elsif ($unit eq 'N') {
107             # $dist = $dist * 0.8684;
108             # }
109             # return ($dist);
110             # }
111             #
112             # my $pi = atan2(1,1) * 4;
113             #
114             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
115             # #::: This function get the arccos function using arctan function :::
116             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
117             # sub _acos {
118             # my ($rad) = @_;
119             # my $ret = atan2(sqrt(1 - $rad**2), $rad);
120             # return $ret;
121             # }
122             #
123             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
124             # #::: This function converts decimal degrees to radians :::
125             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
126             # sub _deg2rad {
127             # my ($deg) = @_;
128             # return ($deg * $pi / 180);
129             # }
130             #
131             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
132             # #::: This function converts radians to decimal degrees :::
133             # #::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
134             # sub _rad2deg {
135             # my ($rad) = @_;
136             # return ($rad * 180 / $pi);
137             # }
138              
139             =head1 AUTHOR
140              
141             Nigel Horne, C<< >>
142              
143             =head1 BUGS
144              
145             =head1 SEE ALSO
146              
147             =over 4
148              
149             =item * L
150              
151             =item * L
152              
153             =back
154              
155             =head1 SUPPORT
156              
157             This module is provided as-is without any warranty.
158              
159             Please report any bugs or feature requests to C,
160             or through the web interface at
161             L.
162             I will be notified, and then you'll
163             automatically be notified of progress on your bug as I make changes.
164              
165             You can find documentation for this module with the perldoc command.
166              
167             perldoc Geo::AnomalyDetector
168              
169             You can also look for information at:
170              
171             =over 4
172              
173             =item * MetaCPAN
174              
175             L
176              
177             =item * RT: CPAN's request tracker
178              
179             L
180              
181             =item * CPAN Testers' Matrix
182              
183             L
184              
185             =item * CPAN Testers Dependencies
186              
187             L
188              
189             =back
190              
191             =head1 LICENSE AND COPYRIGHT
192              
193             Copyright 2025 Nigel Horne.
194              
195             This program is released under the following licence: GPL2
196              
197             =cut
198              
199             1;
200              
201             __END__