File Coverage

blib/lib/GPS/Point/Filter.pm
Criterion Covered Total %
statement 29 91 31.8
branch 9 38 23.6
condition 1 3 33.3
subroutine 7 17 41.1
pod 15 15 100.0
total 61 164 37.2


line stmt bran cond sub pod time code
1             package GPS::Point::Filter;
2 1     1   27111 use strict;
  1         3  
  1         36  
3 1     1   1502 use DateTime;
  1         193394  
  1         1023  
4              
5             our $VERSION='0.02';
6              
7             =head1 NAME
8              
9             GPS::Point::Filter - Algorithm to filter extraneous GPS points
10              
11             =head1 SYNOPSIS
12              
13             use GPS::Point::Filter;
14             my $gpf=GPS::Point::Filter->new;
15             $gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample);
16             my $status=$gpf->addPoint($point);
17              
18             =head1 DESCRIPTION
19              
20             GPS::Point::Filter uses a single pass linear algorithm to filter extraneous GPS points from a GPS feed. The filter uses three tests to determine whether to trigger a callback or not.
21              
22             The most common use for this type of algorithm is to intelligently reduce the number of points before transmission over a limited bandwidth network. The filter properties will need to be tuned for particular networks and implementations.
23              
24             =head1 USAGE
25              
26             use GPS::Point::Filter;
27             my $gpf=GPS::Point::Filter->new;
28             $gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample);
29             my $point=GPS::Point->new(time => time,
30             lat => 39,
31             lon => -77,
32             speed => 25,
33             heading => 135);
34             my $status=$gpf->addPoint($point);
35             printf "%s\n", $status if $status;
36              
37             =head1 CONSTRUCTOR
38              
39             =head2 new
40              
41             my $gpf=GPS::Point::Filter->new(
42             separation => 2000, #meters
43             interlude => 1200, #seconds
44             deviation => 500, #meters
45             );
46              
47             =cut
48              
49             sub new {
50 1     1 1 11 my $this = shift();
51 1   33     7 my $class = ref($this) || $this;
52 1         2 my $self = {};
53 1         3 bless $self, $class;
54 1         6 $self->initialize(@_);
55 1         2 return $self;
56             }
57              
58             =head2 initialize
59              
60             =cut
61              
62             sub initialize {
63 1     1 1 2 my $self = shift();
64 1         5 %$self=@_;
65 1 50       5 $self->separation(2000) unless defined $self->separation;
66 1 50       4 $self->interlude(1200) unless defined $self->interlude;
67 1 50       4 $self->deviation(500) unless defined $self->deviation;
68             }
69              
70             =head1 METHODS (Properties)
71              
72             =head2 interlude
73              
74             Sets or returns the filter interlude property. The interlude is defined as the period of time in seconds for which the previous filter point is still valid or not stale. The filter will trigger a callback if the GPS sample point does not move when the interlude is exceeded.
75              
76             $gpf->interlude(1200); #default is 1200 seconds
77              
78             =cut
79              
80             sub interlude {
81 2     2 1 2 my $self=shift;
82 2 100       6 if (@_) {
83 1         4 $self->{'interlude'}=shift;
84             }
85 2         5 return $self->{'interlude'};
86             }
87              
88             =head2 separation
89              
90             Sets or returns the filter separation property. The separation is defined as the distance in meters between the previous filter point and the sample point. The filter will trigger a callback when then separation is exceeded.
91              
92             $gpf->separation(2000); #default is 2000 meters
93              
94             =cut
95              
96             sub separation {
97 2     2 1 3 my $self=shift;
98 2 100       6 if (@_) {
99 1         3 $self->{'separation'}=shift;
100             }
101 2         8 return $self->{'separation'};
102             }
103              
104             =head2 deviation
105              
106             Sets or returns the filter deviation property. The deviation is defined as the distance in meters between the constant velocity predicted location of the previous filter point and the sample point. The filter will trigger a callback when then deviation is exceeded.
107              
108             $gpf->deviation(500); #default is 500 meters
109              
110             =cut
111              
112             sub deviation {
113 2     2 1 3 my $self=shift;
114 2 100       6 if (@_) {
115 1         2 $self->{'deviation'}=shift;
116             }
117 2         5 return $self->{'deviation'};
118             }
119              
120             =head1 METHODS
121              
122             =head2 addCallback
123              
124             Add a sub reference to the callback hash and returns the $gpf object.
125              
126             $gpf->addCallback(label=>sub{print shift->latlon."\n"});
127             $gpf->addCallback(label=>\&mysub);
128             sub mysub {
129             my $point=shift;#GPS::Point (new point)
130             my $gpf=shift; #GPS::Point::Filter with state info and previous point
131             printf "Lat: %s, Lon: %s\n", $point->latlon;
132             }
133              
134             =cut
135              
136             sub addCallback {
137 0     0 1   my $self=shift;
138 0 0         if (scalar(@_) == 2) {
139 0           my $key=shift;
140 0           my $value=shift;
141 0 0         $self->{'callback'}={} unless ref($self->{'callback'}) eq "HASH";
142 0           $self->{'callback'}->{$key}=$value;
143             } else {
144 0           die("Error: Method addCallback requires two arguments.");
145             }
146 0           return $self;
147             }
148              
149             =head2 deleteCallback
150              
151             my $sub=$gpf->deleteCallback("label");
152              
153             =cut
154              
155             sub deleteCallback {
156 0     0 1   my $self=shift;
157 0           my $label=shift;
158 0           return delete $self->{'callback'}->{$label};
159             }
160              
161             =head2 addPoint
162              
163             Adds a point to the filter to be tested and returns a short staus string. If the point is "extraneous", then the filter will not trigger a callback.
164              
165             my $point=GPS::Point->new(
166             lat => 39.000, #decimal degrees
167             lon => -77.000, #decimal degrees
168             speed => 50.0, #meters/second
169             heading => 45.0, #degrees clockwise from North
170             );
171             my $status=$gpf->addPoint($point);
172             if ($status) {print "added"} else {print "filtered"}
173              
174             =cut
175              
176             sub addPoint {
177 0     0 1   my $self=shift;
178 0           my $point=shift;
179 0 0         die("Error: Point needs to be GPS::Point.")
180             unless ref($point) eq "GPS::Point";
181 0 0         die("Error: Point needs to be at least GPS::Point 0.10.")
182             unless $point->VERSION >= 0.10;
183 0 0         unless (defined $self->point) {
184 0           $self->execute($point);
185 0           return $self->status(sprintf("start: %s", DateTime->now->datetime));
186             } else {
187 0           my $interlude=$point->time - $self->point->time;
188 0 0         if ($interlude > $self->interlude) {
189 0           $self->execute($point);
190 0           return $self->status(sprintf("interlude: %s", $interlude));
191             } else {
192 0           my $separation=$point->distance($self->point);
193 0 0         if ($separation > $self->separation) {
194 0           $self->execute($point);
195 0           return $self->status(sprintf("separation: %s", $separation));
196             } else {
197 0           my $track=$self->point->track($interlude);
198 0           my $deviation=$point->distance($track);
199 0 0         print GPS::Point::Filter::callback_sample_string(Track=>$track)
200             if $self->{'debug'};
201 0 0         if ($deviation > $self->deviation) {
202 0           $self->execute($point);
203 0           return $self->status(sprintf("deviation: %s", $deviation));
204             } else {
205 0           return undef;
206             }
207             }
208             }
209             }
210             }
211              
212             =head2 point
213              
214             Sets or returns the GPS point stored in the GPS::Point::Filter object.
215              
216             my $point=$gpf->point;
217              
218             This point is set to the previous filter point when the callback is triggered. But, is updated just after the execute is complete.
219              
220             =cut
221              
222             sub point {
223 0     0 1   my $self=shift;
224 0 0         if (@_) {
225 0           $self->{'point'}=shift;
226             }
227 0           return $self->{'point'};
228             }
229              
230             =head2 count
231              
232             Sets or returns the count of number of points that have been filtered since the previous filter point;
233              
234             $gpf->count;
235              
236             =cut
237              
238             sub count {
239 0     0 1   my $self=shift;
240 0 0         if (@_) {
241 0           $self->{'count'}=shift;
242             }
243 0           return $self->{'count'};
244             }
245              
246             =head2 status
247              
248             Sets or returns the status of the previous filter point.
249              
250             =cut
251              
252             sub status {
253 0     0 1   my $self=shift;
254 0 0         if (@_) {
255 0           $self->{'status'}=shift;
256             }
257 0           return $self->{'status'};
258             }
259              
260              
261             =head1 METHODS (Internal)
262              
263             =head2 callback
264              
265             Returns the callback hash of sub references.
266              
267             my $callback=$gpf->callback; #{label=>sub{}}
268             my %callback=$gpf->callback; #(label=>sub{})
269              
270             Note: Callbacks are executed sorted by key.
271              
272             =cut
273              
274             sub callback {
275 0     0 1   my $self=shift;
276 0 0         return wantarray ? %{$self->{'callback'}} : $self->{'callback'};
  0            
277             }
278              
279             =head2 execute
280              
281             Executes all sub references in the callback hash sorted by key.
282              
283             The $point and the $gpf objects are passed to the sub routine as the first two arguments.
284              
285             $gpf->execute;
286              
287             =cut
288              
289             sub execute {
290 0     0 1   my $self=shift;
291 0           my $point=shift;
292 0           my $callback=$self->callback;
293 0           foreach my $key (sort keys %$callback) {
294 0           &{$callback->{$key}}($point, $self);
  0            
295             }
296 0           $self->point($point);
297 0           return $self;
298             }
299              
300             =head1 Functions (Convenience)
301              
302             =head2 callback_sample
303              
304             A very simple callback example.
305              
306             GPS::Point::Filter::callback_sample_string($point);
307              
308             To register
309              
310             $gpf->addCallback(sample=>\&GPS::Point::Filter::callback_sample);
311              
312             =cut
313              
314             sub callback_sample {
315 0     0 1   my $point=shift;
316 0           my $gpf=shift;
317 0           print &callback_sample_string(Filter=>$point);
318             }
319              
320             =head2 callback_sample_string
321              
322             Returns a formated string given a GPS::Point
323              
324             my $string=GPS::Point::Filter::callback_sample_string($point);
325              
326             =cut
327              
328             sub callback_sample_string {
329 0     0 1   my $label=shift;
330 0           my $point=shift;
331 0           return join("\t", $label, $point->time,
332             $point->latlon,
333             $point->speed,
334             $point->heading). "\n";
335             }
336              
337             =head1 TODO
338              
339             I would like to implement a Kalman Filter in order to filter point data instead of the current interlude, separation, and deviation properties.
340              
341             Add count of points filtered since previous point
342              
343             Add status to gpf object
344              
345             =head1 BUGS
346              
347             Please report bugs to GEO-PERL list
348              
349             =head1 SUPPORT
350              
351             Please Try the GEO-PERL list
352              
353             =head1 AUTHOR
354              
355             Michael R. Davis
356             CPAN ID: MRDVT
357             domain=>michaelrdavis,tld=>com,account=>perl
358              
359             =head1 COPYRIGHT
360              
361             This program is free software licensed under the...
362              
363             The BSD License
364              
365             The full text of the license can be found in the
366             LICENSE file included with this module.
367              
368             =head1 SEE ALSO
369              
370             L, L, L, L, L
371              
372             =cut
373              
374             1;