File Coverage

blib/lib/GPS/Point/Cluster.pm
Criterion Covered Total %
statement 77 85 90.5
branch 31 38 81.5
condition 8 9 88.8
subroutine 19 21 90.4
pod 18 18 100.0
total 153 171 89.4


line stmt bran cond sub pod time code
1             package GPS::Point::Cluster;
2 4     4   98986 use strict;
  4         9  
  4         163  
3 4     4   3411 use Geo::Inverse;
  4         32446  
  4         128  
4 4     4   12265 use DateTime;
  4         756271  
  4         4499  
5              
6             our $VERSION='0.05';
7              
8             =head1 NAME
9              
10             GPS::Point::Cluster - Groups GPS Points in to clusters
11              
12             =head1 SYNOPSIS
13              
14             use GPS::Point::Cluster;
15             my $cluster=GPS::Point::Cluster->new(
16             separation => 500, #meters
17             interlude => 600, #seconds
18             );
19             my @pt=({}, {}, {}, ...); #{lat=>39, lon=>-77, time=>$epoch_seconds}
20              
21             foreach my $pt (@pt) {
22             my $obj=$cluster->merge_attempt($pt);
23             if (defined $obj) {
24             print join(",", $cluster->index, $cluster->start_dt, $cluster->end_dt,
25             $cluster->lat, $cluster->lon, $cluster->weight), "\n";
26             $cluster=$obj;
27             }
28             }
29              
30              
31              
32             =head1 DESCRIPTION
33              
34             =head1 USAGE
35              
36             =head1 CONSTRUCTOR
37              
38             =head2 new
39              
40             my $cluster = GPS::Point::Cluster->new(separation=>500);
41              
42             =cut
43              
44             sub new {
45 11     11 1 582 my $this = shift();
46 11   66     51 my $class = ref($this) || $this;
47 11         20 my $self = {};
48 11         25 bless $self, $class;
49 11         43 $self->initialize(@_);
50 11         29 return $self;
51             }
52              
53             =head1 METHODS
54              
55             =head2 initialize
56              
57             =cut
58              
59             sub initialize {
60 11     11 1 16 my $self = shift();
61 11         89 %$self=@_;
62 11 100       35 $self->GeoInverse(Geo::Inverse->new)
63             unless ref($self->GeoInverse) eq "Geo::Inverse";
64 11 100       35 $self->separation(500) unless $self->separation;
65 11 100       32 $self->interlude(600) unless $self->interlude;
66 11 100       32 $self->index(0) unless $self->index;
67 11 100       29 $self->weight(0) unless $self->weight;
68             }
69              
70             =head2 settings
71              
72             Returns a hash of default settings to transfer from one cluster to the next.
73              
74             my $hash=$cluster->settings;
75             my %hash=$cluster->settings;
76              
77             =cut
78              
79             sub settings {
80 6     6 1 10 my $self=shift;
81 6         20 my @keys=qw{separation interlude GeoInverse};
82 6         22 my %hash=(index=>$self->index + 1, map {$_=>$self->{$_}} @keys);
  18         56  
83 6 50       54 return wantarray ? %hash : \%hash;
84             }
85              
86             =head2 index
87              
88             Returns the cluster index which is a running integer.
89              
90             =cut
91              
92             sub index {
93 81     81 1 25056 my $self=shift;
94 81 100       212 $self->{'index'}=shift if @_;
95 81         348 return $self->{'index'};
96             }
97              
98             =head2 separation
99              
100             The threshold distance in meters between the cluster and the test point.
101              
102             =cut
103              
104             sub separation {
105 71     71 1 483 my $self=shift;
106 71 100       194 $self->{'separation'}=shift if @_;
107 71         595 return $self->{'separation'};
108             }
109              
110             =head2 interlude
111              
112             The threshold duration in seconds between the cluster end time and the test point.
113              
114             =cut
115              
116             sub interlude {
117 72     72 1 93 my $self=shift;
118 72 100       165 $self->{'interlude'}=shift if @_;
119 72         268 return $self->{'interlude'};
120             }
121              
122             =head2 lat
123              
124             Latitude in decimal degrees WGS-84. The latitude is calculated as a mathimatical average of all latitudes that constitute the cluster.
125              
126             =cut
127              
128             sub lat {
129 217     217 1 261 my $self=shift;
130 217 100       478 $self->{'lat'}=shift if @_;
131 217         761 return $self->{'lat'};
132             }
133              
134             =head2 lon
135              
136             Longitude in decimal degrees WGS-84. The longitude is calculated as a mathimatical average of all longitudes that constitute the cluster.
137              
138             =cut
139              
140             sub lon {
141 217     217 1 20267 my $self=shift;
142 217 100       494 $self->{'lon'}=shift if @_;
143 217         780 return $self->{'lon'};
144             }
145              
146             =head2 weight
147              
148             The count of points that constitute the cluster.
149              
150             =cut
151              
152             sub weight {
153 344     344 1 21249 my $self=shift;
154 344 100       727 $self->{'weight'}=shift if @_;
155 344         1099 return $self->{'weight'};
156             }
157              
158             =head2 start
159              
160             Returns the cluster start date time as seconds from epoch
161              
162             =cut
163              
164             sub start {
165 111     111 1 166 my $self=shift;
166 111 50       282 $self->{'start'}=shift if @_;
167 111         361 return $self->{'start'};
168             }
169              
170             =head2 start_dt
171              
172             Returns the cluster start date time as a L object
173              
174             =cut
175              
176             sub start_dt {
177 0     0 1 0 my $self=shift;
178 0 0       0 unless (defined $self->{'start_dt'}) {
179 0         0 $self->{'start_dt'}=DateTime->from_epoch(epoch=>$self->start);
180             }
181 0         0 return $self->{'start_dt'};
182             }
183              
184             =head2 end
185              
186             Returns the cluster end date time as seconds from epoch
187              
188             =cut
189              
190             sub end {
191 164     164 1 204 my $self=shift;
192 164 100       448 $self->{'end'}=shift if @_;
193 164         481 return $self->{'end'};
194             }
195              
196             =head2 end_dt
197              
198             Returns the cluster end date time as a L object
199              
200             =cut
201              
202             sub end_dt {
203 0     0 1 0 my $self=shift;
204 0 0       0 unless (defined $self->{'end_dt'}) {
205 0         0 $self->{'end_dt'}=DateTime->from_epoch(epoch=>$self->end);
206             }
207 0         0 return $self->{'end_dt'};
208             }
209              
210             =head2 GeoInverse
211              
212             Returns a L object which is used to calculate geodetic distances.
213              
214             =cut
215              
216             sub GeoInverse {
217 70     70 1 1549 my $self=shift;
218 70 100       338 $self->{'GeoInverse'}=shift if @_;
219 70         253 return $self->{'GeoInverse'};
220             }
221              
222             =head2 merge_attempt
223              
224             Attempts to merge the point into the cluster. If the point does not fit in the cluster then the method returns a new cluster. If it merged, then it returns undef.
225              
226             my $new_cluster=$cluster->merge_attempt($pt);
227             if (defined $new_cluster) {
228             #New cluster is constructed with $pt as the only member. $cluster is unmodified.
229             } else {
230             #$pt is added the cluster. The cluster is updated appropriately.
231             }
232              
233             =cut
234              
235             sub merge_attempt {
236 58     58 1 126541 my $self=shift;
237 58         96 my $pt=shift;
238 58 100 100     185 if ( $self->weight
      100        
239             and $self->distance($pt) < $self->separation
240             and $self->duration($pt) < $self->interlude ) {
241 52         149 $self->merge($pt);
242 52         128 return undef;
243             } else {
244 6         31 return $self->new(%$pt, $self->settings,
245             start =>$pt->{'time'},
246             end =>$pt->{'time'},
247             weight =>1);
248             }
249             }
250              
251             =head2 distance
252              
253             Returns the distance in meters between the cluster and the point.
254              
255             my $distance=$cluster->distance($pt);
256              
257             =cut
258              
259             sub distance {
260 54     54 1 81 my $self=shift;
261 54         83 my $pt=shift;
262 54         181 my $distance=$self->GeoInverse->inverse($self->lat => $self->lon,
263             $pt->{'lat'} => $pt->{'lon'});
264 54         7051 return $distance;
265             }
266              
267             =head2 duration
268              
269             Returns the duration in seconds between the cluster and the point.
270              
271             my $duration=$cluster->duration($pt);
272              
273             =cut
274              
275             sub duration {
276 53     53 1 81 my $self=shift;
277 53         65 my $pt=shift;
278 53         172 return $pt->{'time'} - $self->end;
279             }
280              
281             =head2 merge
282              
283             Merges point into cluster returns cluster.
284              
285             my $cluster->merge($pt);
286              
287             =cut
288              
289             sub merge {
290 52     52 1 68 my $self=shift;
291 52         65 my $pt=shift;
292 52 50       110 $self->start($pt->{'time'}) unless defined $self->start;
293 52         125 $self->end($pt->{'time'});
294 52         73 $self->{'end_dt'}=undef;
295 52         113 my $weight=$self->weight;
296 52         124 $self->weight($weight+1);
297 52         108 $self->lat(($self->lat * $weight + $pt->{'lat'})/$self->weight);
298 52         117 $self->lon(($self->lon * $weight + $pt->{'lon'})/$self->weight);
299 52         83 return $self;
300             }
301              
302             =head1 BUGS
303              
304             =head1 SUPPORT
305              
306             =head1 AUTHOR
307              
308             Michael R. Davis
309             CPAN ID: MRDVT
310             STOP, LLC
311             domain=>michaelrdavis,tld=>com,account=>perl
312             http://www.stopllc.com/
313              
314             =head1 COPYRIGHT
315              
316             This program is free software licensed under the...
317              
318             The BSD License
319              
320             The full text of the license can be found in the
321             LICENSE file included with this module.
322              
323             =head1 SEE ALSO
324              
325             L, L
326              
327             =cut
328              
329             1;