File Coverage

blib/lib/GPS/Point.pm
Criterion Covered Total %
statement 104 192 54.1
branch 33 82 40.2
condition 11 54 20.3
subroutine 27 35 77.1
pod 31 31 100.0
total 206 394 52.2


line stmt bran cond sub pod time code
1             package GPS::Point;
2 6     6   164723 use strict;
  6         16  
  6         235  
3 6     6   31 use warnings;
  6         15  
  6         268  
4 6     6   33 use Scalar::Util qw{reftype};
  6         24  
  6         18550  
5              
6             our $VERSION = '0.20';
7              
8             =head1 NAME
9              
10             GPS::Point - Provides an object interface for a GPS point.
11              
12             =head1 SYNOPSIS
13              
14             use GPS::Point;
15             my $obj=GPS::Point->newGPSD($GPSD_O_line);#e.g. GPSD,O=....
16             my $obj=GPS::Point->new(
17             time => $time, #float seconds from the unix epoch
18             lat => $lat, #signed degrees
19             lon => $lon, #signed degrees
20             alt => $hae, #meters above the WGS-84 ellipsoid
21             speed => $speed, #meters/second (over ground)
22             heading => $heading, #degrees clockwise from North
23             climb => $climb, #meters/second
24             etime => $etime, #float seconds
25             ehorizontal => $ehz, #float meters
26             evertical => $evert, #float meters
27             espeed => $espeed, #meters/second
28             eheading => $ehead, #degrees
29             eclimb => $eclimb, #meters/second
30             mode => $mode, #GPS mode [?=>undef,None=>1,2D=>2,3D=>3]
31             tag => $tag, #Name of the GPS message for data
32             );
33              
34             =head1 DESCRIPTION
35              
36             This is a re-write of L with the goal of being more re-usable.
37              
38             GPS::Point - Provides an object interface for a GPS fix (e.g. Position, Velocity and Time).
39              
40             Note: Please use Geo::Point, if you want 2D or projection support.
41              
42             =head1 USAGE
43              
44             print scalar($point->latlon), "\n"; #latlon in scalar context
45             my ($x,$y,$z)=$point->ecef; #if Geo::ECEF is available
46             my $GeoPointObject=$point->GeoPoint; #if Geo::Point is available
47             my @distance=$point->distance($point2); #if Geo::Inverse is available
48             my $distance=$point->distance($point2); #if Geo::Inverse->VERSION >=0.05
49              
50             =head1 USAGE TODO
51              
52             my $obj=GPS::Point->newNMEA($NMEA_lines); #e.g. GGA+GSA+RMC
53              
54             =head1 CONSTRUCTORS
55              
56             =head2 new
57              
58             my $obj = GPS::Point->new();
59              
60             =cut
61              
62             sub new {
63 5     5 1 8945 my $this = shift();
64 5   33     41 my $class = ref($this) || $this;
65 5         13 my $self = {};
66 5         14 bless $self, $class;
67 5         21 $self->initialize(@_);
68 5         17 return $self;
69             }
70              
71             =head2 newGPSD
72              
73             my $obj=GPS::Point->newGPSD($GPSD_O_line);#e.g. GPSD,O=....
74              
75             Note: GPSD protocol 2 is soon to be defunct.
76              
77             =cut
78              
79             sub newGPSD {
80 1     1 1 12 my $this = shift();
81 1   33     8 my $class = ref($this) || $this;
82 1         2 my $self = {};
83 1         3 bless $self, $class;
84 1         5 $self->initializeGPSD(@_);
85 1         4 return $self;
86             }
87              
88             =head2 newMulti
89              
90             Constructs a GPS::Point from a Multitude of arguments. Arguments can be a L, L, {lat=>$lat,lon=>$lon} (can be blessed), [$lat, $lon] (can be blessed) or a ($lat, $lon) pair.
91              
92             my $point=GPS::Point->newMulti( $lat, $lon, $alt ); #supports lat, lon and alt
93             my $point=GPS::Point->newMulti([$lat, $lon, $alt]); #supports lat, lon and alt
94             my $point=GPS::Point->newMulti({lat=>$lat, lon=>$lon, ...});
95             my $point=GPS::Point->newMulti(GPS::Point->new(lat=>$lat, lon=>$lon));
96             my $point=GPS::Point->newMulti(Geo::Point->new(lat=>$lat, long=>$lon, proj=>'wgs84'));
97             my $point=GPS::Point->newMulti({latitude=>$lat, longtude=>$lon});
98              
99             Note: Hash reference context supports the following keys lat, lon, alt, latitude, longitude, long, altitude, elevation, hae, elev.
100              
101             Note: Units are always decimal degrees for latitude and longitude and meters above the WGS-84 ellipsoid for altitude.
102              
103             =cut
104              
105             sub newMulti {
106 7     7 1 32 my $this = shift();
107 7   33     37 my $class = ref($this) || $this;
108 7         12 my $self = {};
109 7         15 bless $self, $class;
110 7         25 $self->initializeMulti(@_);
111 7         21 return $self;
112             }
113              
114             =head2 initialize, initializeGPSD, initializeMulti
115              
116             =cut
117              
118             sub initialize {
119 5     5 1 12 my $self = shift();
120 5         37 %$self=@_;
121             }
122              
123             sub initializeGPSD {
124 1     1 1 2 my $self=shift();
125 1         1 my $line=shift(); #GPSD,O=MID2 1175911006.190 ? 53.527185 -113.530093 705.51 4.00 3.49 0.0000 0.074 0.101 ? 8.00 6.99 3
126 1         7 my @line=split(/,/, $line);
127 1 50       4 warn("Warning: Expected GPSD formatted line.") unless $line[0] eq "GPSD";
128 1         2 my $obj=undef();
129 1         4 foreach (@line) { #I pull the last one if O=?,O=?,...
130 2         7 my @rpt=split(/=/, $_);
131 2 100       10 if ($rpt[0] eq 'O') {
132 1         20 my @data=map {&_q2u($_)} split(/\s+/, $rpt[1]);
  15         28  
133 1         27 %$self=(tag => $data[ 0],
134             time => $data[ 1],
135             etime => $data[ 2],
136             lat => $data[ 3],
137             lon => $data[ 4],
138             alt => $data[ 5],
139             ehorizontal => $data[ 6],
140             evertical => $data[ 7],
141             heading => $data[ 8],
142             speed => $data[ 9],
143             climb => $data[10],
144             eheading => $data[11],
145             espeed => $data[12],
146             eclimb => $data[13],
147             mode => $data[14]);
148             }
149             }
150             }
151              
152             sub initializeMulti {
153 7     7 1 9 my $self=shift;
154 7         10 my $point=shift;
155 7 50       80 if (!ref($point)) {
    50          
    50          
    50          
    100          
    50          
156 0   0     0 $self->{'lat'}=$point ||0;
157 0   0     0 $self->{'lon'}=shift ||0;
158 0   0     0 $self->{'alt'}=shift ||0;
159             } elsif (ref($point) eq "Geo::Point") {
160 0 0       0 $point=$point->in('wgs84') unless $point->proj eq "wgs84";
161 0   0     0 $self->{'lat'}=$point->latitude ||0;
162 0   0     0 $self->{'lon'}=$point->longitude ||0;
163             } elsif (ref($point) eq "GPS::Point") {
164 0         0 %$self=%$point;
165             } elsif (ref($point) eq "Net::GPSD::Point") {
166 0         0 $self->{'time'}=$point->time;
167 0   0     0 $self->{'lat'}=$point->latitude ||0;
168 0   0     0 $self->{'lon'}=$point->longitude ||0;
169 0   0     0 $self->{'alt'}=$point->altitude ||0;
170 0         0 $self->{'speed'}=$point->speed;
171 0         0 $self->{'heading'}=$point->heading;
172 0         0 $self->{'climb'}=$point->climb;
173 0         0 $self->{'etime'}=$point->errortime;
174 0         0 $self->{'ehorizontal'}=$point->errorhorizontal;
175 0         0 $self->{'evertical'}=$point->errorvertical;
176 0         0 $self->{'espeed'}=$point->errorspeed;
177 0         0 $self->{'eheading'}=$point->errorheading;
178 0         0 $self->{'eclimb'}=$point->errorclimb;
179 0         0 $self->{'mode'}=$point->mode;
180 0         0 $self->{'tag'}=$point->tag;
181             } elsif (reftype($point) eq "HASH") {
182 5         27 %$self=%$point;
183 5   50     30 $self->{'lat'}=$point->{'lat'} ||
184             delete($point->{'latitude'}) ||0;
185 5   50     34 $self->{'lon'}=$point->{'lon'} ||
186             delete($point->{'long'}) ||
187             delete($point->{'longitude'}) ||0;
188 5   50     48 $self->{'alt'}=$point->{'alt'} ||
189             delete($point->{'altitude'}) ||
190             delete($point->{'elevation'}) ||
191             delete($point->{'hae'}) ||
192             delete($point->{'elev'}) ||0;
193             } elsif (reftype($point) eq "ARRAY") {
194 2   50     8 $self->{'lat'}=$point->[0] ||0;
195 2   50     10 $self->{'lon'}=$point->[1] ||0;
196 2   50     27 $self->{'alt'}=$point->[2] ||0;
197             }
198             }
199              
200             =head1 METHODS (Base)
201              
202             =head2 time
203              
204             Sets or returns seconds since the Unix epoch, UTC (float, seconds)
205              
206             print $obj->time, "\n";
207              
208             =cut
209              
210             sub time {
211 2     2 1 8 my $self = shift();
212 2 50       10 $self->{'time'}=shift() if @_;
213 2         13 return $self->{'time'};
214             }
215              
216             =head2 lat, latitude
217              
218             Sets or returns Latitude (float, degrees)
219              
220             print $obj->lat, "\n";
221              
222             =cut
223              
224             *latitude=\⪫
225              
226             sub lat {
227 17     17 1 4667 my $self=shift;
228 17 100       57 $self->{'lat'}=shift if @_;
229 17         75 return $self->{'lat'};
230             }
231              
232             =head2 lon, long, longitude
233              
234             Sets or returns Longitude (float, degrees)
235              
236             print $obj->lon, "\n";
237              
238             =cut
239              
240             *longitude=\&lon;
241             *long=\&lon;
242              
243             sub lon {
244 13     13 1 24 my $self = shift();
245 13 50       45 $self->{'lon'}=shift() if @_;
246 13         58 return $self->{'lon'};
247             }
248              
249             =head2 alt, altitude, hae, elevation
250              
251             Sets or returns Altitude (float, meters)
252              
253             print $obj->alt, "\n";
254              
255             =cut
256              
257             *altitude=\&alt;
258             *hae=\&alt;
259             *elevation=\&alt;
260              
261             sub alt {
262 9     9 1 18 my $self = shift();
263 9 50       27 $self->{'alt'}=shift() if @_;
264 9         41 return $self->{'alt'};
265             }
266              
267             =head2 speed
268              
269             Sets or returns speed (float, meters/sec)
270              
271             print $obj->speed, "\n";
272              
273             =cut
274              
275             sub speed {
276 1     1 1 4 my $self = shift();
277 1 50       5 $self->{'speed'}=shift() if @_;
278 1         5 return $self->{'speed'};
279             }
280              
281             =head2 heading, bearing
282              
283             Sets or returns heading (float, degrees)
284              
285             print $obj->heading, "\n";
286              
287             =cut
288              
289             *bearing=\&heading;
290              
291             sub heading {
292 2     2 1 4 my $self = shift();
293 2 50       8 $self->{'heading'}=shift() if @_;
294 2         10 return $self->{'heading'};
295             }
296              
297             =head2 climb
298              
299             Sets or returns vertical velocity (float, meters/sec)
300              
301             print $obj->climb, "\n";
302              
303             =cut
304              
305             sub climb {
306 1     1 1 2 my $self = shift();
307 1 50       5 $self->{'climb'}=shift() if @_;
308 1         6 return $self->{'climb'};
309             }
310              
311             =head2 etime
312              
313             Sets or returns estimated timestamp error (float, seconds, 95% confidence)
314              
315             print $obj->etime, "\n";
316              
317             =cut
318              
319             sub etime {
320 1     1 1 2 my $self = shift();
321 1 50       5 $self->{'etime'}=shift() if @_;
322 1         6 return $self->{'etime'};
323             }
324              
325             =head2 ehorizontal
326              
327             Sets or returns horizontal error estimate (float, meters)
328              
329             print $obj->ehorizontal, "\n";
330              
331             =cut
332              
333             sub ehorizontal {
334 1     1 1 2 my $self = shift();
335 1 50       3 $self->{'ehorizontal'}=shift() if @_;
336 1         5 return $self->{'ehorizontal'};
337             }
338              
339             =head2 evertical
340              
341             Sets or returns vertical error estimate (float, meters)
342              
343             print $obj->evertical, "\n";
344              
345             =cut
346              
347             sub evertical {
348 1     1 1 2 my $self = shift();
349 1 50       5 $self->{'evertical'}=shift() if @_;
350 1         6 return $self->{'evertical'};
351             }
352              
353             =head2 espeed
354              
355             Sets or returns error estimate for speed (float, meters/sec, 95% confidence)
356              
357             print $obj->espeed, "\n";
358              
359             =cut
360              
361             sub espeed {
362 1     1 1 3 my $self = shift();
363 1 50       6 $self->{'espeed'}=shift() if @_;
364 1         5 return $self->{'espeed'};
365             }
366              
367             =head2 eheading
368              
369             Sets or returns error estimate for course (float, degrees, 95% confidence)
370              
371             print $obj->eheading, "\n";
372              
373             =cut
374              
375             sub eheading {
376 1     1 1 2 my $self = shift();
377 1 50       5 $self->{'eheading'}=shift() if @_;
378 1         5 return $self->{'eheading'};
379             }
380              
381             =head2 eclimb
382              
383             Sets or returns Estimated error for climb/sink (float, meters/sec, 95% confidence)
384              
385             print $obj->eclimb, "\n";
386              
387             =cut
388              
389             sub eclimb {
390 1     1 1 3 my $self = shift();
391 1 50       5 $self->{'eclimb'}=shift() if @_;
392 1         5 return $self->{'eclimb'};
393             }
394              
395             =head2 mode
396              
397             Sets or returns the NMEA mode (integer; undef=>no mode value yet seen, 1=>no fix, 2=>2D, 3=>3D)
398              
399             print $obj->mode, "\n";
400              
401             =cut
402              
403             sub mode {
404 11     11 1 15 my $self = shift();
405 11 100       30 $self->{'mode'}=shift() if @_;
406 11         54 return $self->{'mode'};
407             }
408              
409             =head2 tag
410              
411             Sets or returns a tag identifying the last sentence received. For NMEA devices this is just the NMEA sentence name; the talker-ID portion may be useful for distinguishing among results produced by different NMEA talkers in the same wire. (string)
412              
413             print $obj->tag, "\n";
414              
415             =cut
416              
417             sub tag {
418 1     1 1 832 my $self = shift();
419 1 50       4 $self->{'tag'}=shift() if @_;
420 1         6 return $self->{'tag'};
421             }
422              
423             =head1 METHODS (Value Added)
424              
425             =head2 fix
426              
427             Returns either 1 or 0 based upon if the GPS point is from a valid fix or not.
428              
429             print $obj->fix, "\n";
430              
431             At a minimum this method requires mode to be set.
432              
433             =cut
434              
435             sub fix {
436 4     4 1 8 my $self=shift;
437 4 100 66     9 if (defined($self->mode) and $self->mode > 1) {
438 3         17 return 1;
439             } else {
440 1         4 return 0;
441             }
442             }
443              
444             =head2 datetime
445              
446             Returns a L object from time
447              
448             my $dt=$point->datetime;
449              
450             At a minimum this method requires time to be set.
451              
452             =cut
453              
454             sub datetime {
455 0     0 1 0 my $self=shift;
456 0         0 eval 'use DateTime';
457 0 0       0 if ($@) {
458 0         0 die("Error: The datetime method requires DateTime");
459             } else {
460 0         0 return DateTime->from_epoch(epoch=>$self->time);
461             }
462             }
463              
464             =head2 latlon, latlong
465              
466             Returns Latitude, Longitude as an array in array context and as a space joined string in scalar context
467              
468             my @latlon=$point->latlon;
469             my $latlon=$point->latlon;
470              
471             At a minimum this method requires lat and lon to be set.
472              
473             =cut
474              
475             *latlong=\&latlon;
476              
477             sub latlon {
478 2     2 1 5 my $self = shift();
479 2         6 my @latlon=($self->lat, $self->lon);
480 2 100       14 return wantarray ? @latlon : join(" ", @latlon);
481             }
482              
483             =head2 setAltitude
484              
485             Sets altitude from USGS web service and then returns the GPS::Point object. This method is a wrapper around L.
486              
487             my $point=GPS::Point->new(lat=>$lat, lon=>$lon)->setAltitude;
488             $point->setAltitude;
489             my $alt=$point->alt;
490              
491             At a minimum this method requires lat and lon to be set and alt to be undef.
492              
493             =cut
494              
495             sub setAltitude {
496 0     0 1 0 my $self=shift;
497 0 0       0 unless (defined $self->alt) {
498 0         0 eval 'use Geo::WebService::Elevation::USGS';
499 0 0       0 if ($@) {
500 0         0 die("Error: The setAltitude method requires Geo::WebService::Elevation::USGS");
501             } else {
502 0         0 my $eq=Geo::WebService::Elevation::USGS->new(units=>"METERS", croak=>0);
503 0         0 my $return=$eq->getElevation($self); #Assume this is HAE WGS-84
504 0 0       0 $self->alt($return->{'Elevation'}) if ref($return) eq "HASH";
505             }
506             }
507 0         0 return $self;
508             }
509              
510             =head2 ecef
511              
512             Returns ECEF coordinates. This method is a wrapper around L.
513              
514             my ($x,$y,$z) = $point->ecef;
515             my @xyz = $point->ecef;
516             my $xyz_aref = $point->ecef; #if Geo::ECEF->VERSION >= 0.08
517              
518             At a minimum this method requires lat and lon to be set. (alt of 0 is assumed by Geo::ECEF->ecef).
519              
520             =cut
521              
522             sub ecef {
523 0     0 1 0 my $self=shift;
524 0         0 eval 'use Geo::ECEF';
525 0 0       0 die("Error: The ecef method requires Geo::ECEF") if $@;
526 0 0       0 die("Error: The found geo::ecef not Geo::ECEF.") unless Geo::ECEF->can("new");
527 0         0 my $obj=Geo::ECEF->new;
528 0         0 return $obj->ecef($self->lat, $self->lat, $self->alt);
529             }
530              
531             =head2 GeoPoint
532              
533             Returns a L Object in the WGS-84 projection.
534              
535             my $GeoPointObject = $point->GeoPoint;
536              
537             At a minimum this method requires lat and lon to be set.
538              
539             =cut
540              
541             sub GeoPoint {
542 0     0 1 0 my $self = shift();
543 0         0 eval 'use Geo::Point';
544 0 0       0 if ($@) {
545 0         0 die("Error: The GeoPoint method requires Geo::Point");
546             } else {
547 0         0 return Geo::Point->new(lat=>$self->lat, long=>$self->lon, proj=>'wgs84');
548             }
549             }
550              
551             =head2 distance
552              
553             Returns distance in meters between the object point and the argument point. The argument can be any valid argument of newMulti constructor. This method is a wrapper around Geo::Inverse.
554              
555             my ($faz, $baz, $dist) = $point->distance($pt2); #Array context
556             my $dist = $point->distance($lat, $lon); #if Geo::Inverse->VERSION >=0.05
557              
558             At a minimum this method requires lat and lon to be set.
559              
560             =cut
561              
562             sub distance {
563 0     0 1 0 my $self=shift;
564 0         0 my $point=$_[0];
565 0 0       0 $point=GPS::Point->newMulti(@_) unless ref($point) eq "GPS::Point";
566 0 0       0 if (defined $point) {
567 0         0 eval 'use Geo::Inverse';
568 0 0       0 if ($@) {
569 0         0 die("Error: The distance method requires Geo::Inverse");
570             } else {
571 0         0 my $gi=Geo::Inverse->new;
572 0         0 return $gi->inverse($self->latlon, $point->latlon);
573             }
574             } else {
575 0         0 die(qq{Error: Could not create point from parameters.});
576             }
577             }
578              
579             =head2 track
580              
581             Returns a point object at the predicted location in time seconds assuming constant velocity. Using L calculation.
582              
583             my $new_point=$point->track($seconds); #default $point->heading
584             my $new_point=$point->track($seconds => $heading);
585              
586             At a minimum this method requires lat and lon to be set. It might be very useful to have speed, heading and time set although they all default to zero.
587              
588             =cut
589              
590             sub track {
591 0     0 1 0 my $self=shift;
592 0   0     0 my $seconds=shift||0; #seconds
593 0         0 my $heading=shift; #degrees
594 0 0 0     0 $heading=$self->heading || 0 unless defined $heading; #support 0 degrees passed
595 0   0     0 my $speed=$self->speed || 0; #m/s
596 0         0 my $dist=$speed * $seconds; #meters
597 0         0 my $point=$self->forward($dist => $heading);
598 0   0     0 $point->time(($self->time||0) + $seconds);
599 0         0 return $point;
600             }
601              
602             =head2 forward
603              
604             Returns a point object at the distance and heading using L calculations.
605              
606             my $point=$point->forward($dist); #default $point->heading
607             my $point=$point->forward($dist => $heading); #meters => degrees
608              
609             At a minimum this method requires lat and lon to be set. It might be useful to have heading set although the default is zero.
610              
611             =cut
612              
613             sub forward {
614 0     0 1 0 my $self=shift;
615 0   0     0 my $dist=shift || 0; #meters
616 0         0 my $faz=shift; #degrees
617 0 0 0     0 $faz=$self->heading || 0 unless defined $faz;
618 0         0 eval 'use Geo::Forward';
619 0 0       0 if ($@) {
620 0         0 die("Error: The track method requires Geo::Forward");
621             } else {
622 0         0 my $gf=Geo::Forward->new;
623 0         0 my ($lat2,$lon2,$baz) = $gf->forward($self->latlon, $faz, $dist);
624 0         0 my $point=GPS::Point->new(%$self);
625 0         0 $point->lat($lat2);
626 0         0 $point->lon($lon2);
627 0         0 return $point;
628             }
629             }
630              
631             =head2 buffer
632              
633             Returns a list of L objects equidistant from the current object location.
634              
635             my @buffer=$point->buffer($radius_meters, $sections); #returns (GPS::Point, GPS::Point, ...)
636             my $buffer=$point->buffer($radius_meters, $sections); #returns [GPS::Point, GPS::Point, ...]
637              
638             =cut
639              
640             sub buffer {
641 0     0 1 0 my $self=shift;
642 0         0 my $radius=shift; #meters
643 0   0     0 my $sections=shift || 60; #60 sections = 61 verticies
644 0         0 my @buffer=();
645 0         0 my $arc=360/$sections; #not zero!
646 0         0 foreach my $step (0 .. $sections) {
647 0         0 my $angle=$arc * $step;
648              
649 0         0 push @buffer, $self->forward($radius => $angle);
650             }
651 0 0       0 return wantarray ? @buffer : \@buffer;
652             }
653              
654             sub _q2u {
655 15     15   20 my $a=shift();
656 15 100       229 return $a eq '?' ? undef() : $a;
657             }
658              
659             =head1 BUGS
660              
661             Please log on RT and send email to GPSD-DEV or GEO-PERL email lists.
662              
663             =head1 SUPPORT
664              
665             DavisNetworks.com supports all Perl applications including this package.
666              
667             =head1 AUTHOR
668              
669             Michael R. Davis
670             CPAN ID: MRDVT
671             DavisNetworks.com
672             account=>perl,tld=>com,domain=>michaelrdavis
673             http://www.davisnetworks.com/
674              
675             =head1 COPYRIGHT
676              
677             This program is free software licensed under the...
678              
679             The BSD License
680              
681             The full text of the license can be found in the LICENSE file included with this module.
682              
683             =head1 SEE ALSO
684              
685             L, L, L, L, L, L, L, L, L, L, L
686              
687             =cut
688              
689             1;