File Coverage

blib/lib/Geo/Gpx.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package Geo::Gpx;
2              
3 9     9   460954 use warnings;
  9         27  
  9         478  
4 9     9   50 use strict;
  9         17  
  9         477  
5              
6 9     9   48 use Carp;
  9         41  
  9         893  
7 9     9   27721 use DateTime::Format::ISO8601;
  9         2678643  
  9         624  
8 9     9   115 use DateTime;
  9         19  
  9         231  
9 9     9   12782 use HTML::Entities qw( encode_entities encode_entities_numeric );
  9         79017  
  9         1197  
10 9     9   92 use Scalar::Util qw( blessed );
  9         19  
  9         463  
11 9     9   18455 use Time::Local;
  9         24293  
  9         1704  
12 9     9   13242 use XML::Descent;
  0            
  0            
13              
14             =head1 NAME
15              
16             Geo::Gpx - Create and parse GPX files.
17              
18             =head1 VERSION
19              
20             This document describes Geo::Gpx version 0.26
21              
22             =head1 SYNOPSIS
23              
24             # Version 0.10 compatibility
25             use Geo::Gpx;
26             my $gpx = Geo::Gpx->new( @waypoints );
27             my $xml = $gpx->xml;
28              
29             # New API, generate GPX
30             my $gpx = Geo::Gpx->new();
31             $gpx->waypoints( \@wpt );
32             my $xml = $gpx->xml( '1.0' );
33              
34             # Parse GPX
35             my $gpx = Geo::Gpx->new( xml => $xml );
36             my $waypoints = $gpx->waypoints();
37             my $tracks = $gpx->tracks();
38              
39             # Parse GPX from open file
40             my $gpx = Geo::Gpx->new( input => $fh );
41             my $waypoints = $gpx->waypoints();
42             my $tracks = $gpx->tracks();
43              
44             =head1 DESCRIPTION
45              
46             The original goal of this module was to produce GPX/XML files which were
47             parseable by both GPX Spinner and EasyGPS. As of version 0.13 it has
48             been extended to support general parsing and generation of GPX data. GPX
49             1.0 and 1.1 are supported.
50              
51             =cut
52              
53             use vars qw ($VERSION);
54             $VERSION = '0.26';
55              
56             # Values that are encoded as attributes
57             my %AS_ATTR = (
58             wpt => qr{^lat|lon$},
59             rtept => qr{^lat|lon$},
60             trkpt => qr{^lat|lon$},
61             email => qr{^id|domain$},
62             link => qr{^href$}
63             );
64              
65             my %KEY_ORDER = (
66             wpt => [
67             qw(
68             ele time magvar geoidheight name cmt desc src link sym type fix
69             sat hdop vdop pdop ageofdgpsdata dgpsid extensions
70             )
71             ],
72             );
73              
74             # Map hash keys to GPX names
75             my %XMLMAP = (
76             waypoints => { waypoints => 'wpt' },
77             routes => {
78             routes => 'rte',
79             points => 'rtept'
80             },
81             tracks => {
82             tracks => 'trk',
83             segments => 'trkseg',
84             points => 'trkpt'
85             }
86             );
87              
88             my @META;
89             my @ATTR;
90              
91             BEGIN {
92             @META = qw( name desc author time keywords copyright link );
93             @ATTR = qw( waypoints tracks routes version );
94              
95             # Generate accessors
96             for my $attr ( @META, @ATTR ) {
97             no strict 'refs';
98             *{ __PACKAGE__ . '::' . $attr } = sub {
99             my $self = shift;
100             $self->{$attr} = shift if @_;
101             return $self->{$attr};
102             };
103             }
104             }
105              
106             sub _parse_time {
107             my ( $self, $str ) = @_;
108             my $dt = DateTime::Format::ISO8601->parse_datetime( $str );
109             return $self->{use_datetime} ? $dt : $dt->epoch;
110             }
111              
112             sub _format_time {
113             my ( $self, $tm, $legacy ) = @_;
114             unless ( blessed $tm && $tm->can( 'strftime' ) ) {
115             return $self->_format_time(
116             DateTime->from_epoch(
117             epoch => $tm,
118             time_zone => 'UTC'
119             ),
120             $legacy
121             );
122             }
123              
124             my $ts = $tm->strftime(
125             $legacy
126             ? '%Y-%m-%dT%H:%M:%S.%7N%z'
127             : '%Y-%m-%dT%H:%M:%S%z'
128             );
129             $ts =~ s/(\d{2})$/:$1/;
130             return $ts;
131             }
132              
133             # For backwards compatibility
134             sub _init_legacy {
135             my $self = shift;
136              
137             $self->{keywords} = [qw(cache geocache groundspeak)];
138             $self->{author} = {
139             name => 'Groundspeak',
140             email => {
141             id => 'contact',
142             domain => 'groundspeak.com'
143             }
144             };
145             $self->{desc} = 'GPX file generated by Geo::Gpx';
146             $self->{schema} = [
147             qw(
148             http://www.groundspeak.com/cache/1/0
149             http://www.groundspeak.com/cache/1/0/cache.xsd
150             )
151             ];
152              
153             require Geo::Cache;
154              
155             $self->{handler} = {
156             create => sub {
157             return Geo::Cache->new( @_ );
158             },
159             time => sub {
160             return $self->_format_time( $_[0], 1 );
161             },
162             };
163             }
164              
165             sub _init_shiny_new {
166             my ( $self, $args ) = @_;
167              
168             $self->{use_datetime} = $args->{use_datetime} || 0;
169              
170             $self->{schema} = [];
171              
172             $self->{handler} = {
173             create => sub {
174             return {@_};
175             },
176             time => sub {
177             return $self->_format_time( $_[0], 0 );
178             },
179             };
180             }
181              
182             =head1 INTERFACE
183              
184             =head2 C
185              
186             The original purpose of C was to allow an array of
187             L objects to be converted into a GPX file. This behaviour is
188             maintained by this release:
189              
190             use Geo::Gpx;
191             my $gpx = Geo::Gpx->new( @waypoints );
192             my $xml = $gpx->xml;
193              
194             New applications can use C to parse a GPX file :
195              
196             my $gpx = Geo::Gpx->new( xml => $gpx_document );
197              
198             or from an open filehandle :
199              
200             my $gpx = Geo::Gpx->new( input => $fh );
201              
202             or can create an empty container to which waypoints, routes and tracks
203             can then be added:
204              
205             my $gpx = Geo::Gpx->new();
206             $gpx->waypoints( \@wpt );
207              
208             The following additional options can be specified:
209              
210             =over
211              
212             =item C< use_datetime >
213              
214             If true time values in parsed GPX will be L objects rather
215             than epoch times.
216              
217             =back
218              
219             =cut
220              
221             sub new {
222             my ( $class, @args ) = @_;
223             my $self = bless( {}, $class );
224              
225             # CORE::time because we have our own time method.
226             $self->{time} = CORE::time();
227              
228             # Has to handle same calling convention as previous
229             # version.
230             if ( blessed $args[0] && $args[0]->isa( 'Geo::Cache' ) ) {
231             $self->_init_legacy();
232             $self->{waypoints} = \@args;
233             }
234             elsif ( @args % 2 == 0 ) {
235             my %args = @args;
236             $self->_init_shiny_new( \%args );
237              
238             if ( exists $args{input} ) {
239             $self->_parse( $args{input} );
240             }
241             elsif ( exists $args{xml} ) {
242             $self->_parse( \$args{xml} );
243             }
244             }
245             else {
246             croak( "Invalid arguments" );
247             }
248              
249             return $self;
250             }
251              
252             # Not a method
253             sub _trim {
254             my $str = shift;
255             $str =~ s/^\s+//;
256             $str =~ s/\s+$//;
257             $str =~ s/\s+/ /g;
258             return $str;
259             }
260              
261             sub _parse {
262             my $self = shift;
263             my $source = shift;
264              
265             my $p = XML::Descent->new( { Input => $source } );
266              
267             $p->on(
268             gpx => sub {
269             my ( $elem, $attr ) = @_;
270              
271             $p->context( $self );
272              
273             my $version = $self->{version} = ( $attr->{version} || '1.0' );
274              
275             my $parse_deep = sub {
276             my ( $elem, $attr ) = @_;
277             my $ob = $attr; # Get attributes
278             $p->context( $ob );
279             $p->walk();
280             return $ob;
281             };
282              
283             # Parse a point
284             my $parse_point = sub {
285             my ( $elem, $attr ) = @_;
286             my $pt = $parse_deep->( $elem, $attr );
287             return $self->{handler}->{create}->( %{$pt} );
288             };
289              
290             $p->on(
291             '*' => sub {
292             my ( $elem, $attr, $ctx ) = @_;
293             $ctx->{$elem} = _trim( $p->text() );
294             },
295             time => sub {
296             my ( $elem, $attr, $ctx ) = @_;
297             my $tm = $self->_parse_time( _trim( $p->text() ) );
298             $ctx->{$elem} = $tm if defined $tm;
299             }
300             );
301              
302             if ( _cmp_ver( $version, '1.1' ) >= 0 ) {
303              
304             # Handle 1.1 metadata
305             $p->on(
306             metadata => sub {
307             $p->walk();
308             },
309             [ 'link', 'email', 'author' ] => sub {
310             my ( $elem, $attr, $ctx ) = @_;
311             $ctx->{$elem} = $parse_deep->( $elem, $attr );
312             }
313             );
314             }
315             else {
316              
317             # Handle 1.0 metadata
318             $p->on(
319             url => sub {
320             my ( $elem, $attr, $ctx ) = @_;
321             $ctx->{link}->{href} = _trim( $p->text() );
322             },
323             urlname => sub {
324             my ( $elem, $attr, $ctx ) = @_;
325             $ctx->{link}->{text} = _trim( $p->text() );
326             },
327             author => sub {
328             my ( $elem, $attr, $ctx ) = @_;
329             $ctx->{author}->{name} = _trim( $p->text() );
330             },
331             email => sub {
332             my ( $elem, $attr, $ctx ) = @_;
333             my $em = _trim( $p->text() );
334             if ( $em =~ m{^(.+)\@(.+)$} ) {
335             $ctx->{author}->{email} = {
336             id => $1,
337             domain => $2
338             };
339             }
340             }
341             );
342             }
343              
344             $p->on(
345             bounds => sub {
346             my ( $elem, $attr, $ctx ) = @_;
347             $ctx->{$elem} = $parse_deep->( $elem, $attr );
348             },
349             keywords => sub {
350             my ( $elem, $attr ) = @_;
351             $self->{keywords}
352             = [ map { _trim( $_ ) } split( /,/, $p->text() ) ];
353             },
354             wpt => sub {
355             my ( $elem, $attr ) = @_;
356             push @{ $self->{waypoints} }, $parse_point->( $elem, $attr );
357             },
358             [ 'trkpt', 'rtept' ] => sub {
359             my ( $elem, $attr, $ctx ) = @_;
360             push @{ $ctx->{points} }, $parse_point->( $elem, $attr );
361             },
362             rte => sub {
363             my ( $elem, $attr ) = @_;
364             my $rt = $parse_deep->( $elem, $attr );
365             push @{ $self->{routes} }, $rt;
366             },
367             trk => sub {
368             my ( $elem, $attr ) = @_;
369             my $tk = {};
370             $p->context( $tk );
371             $p->on(
372             trkseg => sub {
373             my ( $elem, $attr ) = @_;
374             my $seg = $parse_deep->( $elem, $attr );
375             push @{ $tk->{segments} }, $seg;
376             }
377             );
378             $p->walk();
379             push @{ $self->{tracks} }, $tk;
380             }
381             );
382              
383             $p->walk();
384             }
385             );
386              
387             $p->walk();
388             }
389              
390             =head2 C
391              
392             Add one or more waypoints. Each waypoint must be a reference to a
393             hash. Each waypoint must include the keys C and C and may
394             include others:
395              
396             my $wpt = {
397             lat => 54.786989,
398             lon => -2.344214,
399             ele => 512,
400             time => 1164488503,
401             magvar => 0,
402             geoidheight => 0,
403             name => 'My house & home',
404             cmt => 'Where I live',
405             desc => '<>',
406             src => 'Testing',
407             link => {
408             href => 'http://hexten.net/',
409             text => 'Hexten',
410             type => 'Blah'
411             },
412             sym => 'pin',
413             type => 'unknown',
414             fix => 'dgps',
415             sat => 3,
416             hdop => 10,
417             vdop => 10,
418             pdop => 10,
419             ageofdgpsdata => 45,
420             dgpsid => 247
421             };
422              
423             $gpx->add_waypoint( $wpt );
424              
425             Time values may either be an epoch offset or a L. If you wish
426             to specify the timezone use a L.
427              
428             =cut
429              
430             sub add_waypoint {
431             my $self = shift;
432              
433             for my $wpt ( @_ ) {
434             eval { keys %$wpt };
435             croak "waypoint argument must be a hash reference"
436             if $@;
437              
438             croak "'lat' and 'lon' keys are mandatory in waypoint hash"
439             unless exists $wpt->{lon} && exists $wpt->{lat};
440              
441             push @{ $self->{waypoints} }, $wpt;
442             }
443             }
444              
445             # Not a method
446             sub _iterate_points {
447             my $pts = shift || []; # array ref
448              
449             unless ( defined $pts ) {
450             return sub {
451             return;
452             };
453             }
454              
455             my $max = scalar( @{$pts} );
456             my $pos = 0;
457             return sub {
458             return if $pos >= $max;
459             return $pts->[ $pos++ ];
460             };
461             }
462              
463             # Not a method
464             sub _iterate_iterators {
465             my @its = @_;
466             return sub {
467             for ( ;; ) {
468             return undef unless @its;
469             my $next = $its[0]->();
470             return $next if defined $next;
471             shift @its;
472             }
473             }
474             }
475              
476             =head2 C
477              
478             Get an iterator that visits all the waypoints in a C.
479              
480             =cut
481              
482             sub iterate_waypoints {
483             my $self = shift;
484             return _iterate_points( $self->{waypoints} );
485             }
486              
487             =head2 C
488              
489             Get an iterator that visits all the routepoints in a C.
490              
491             =cut
492              
493             sub iterate_routepoints {
494             my $self = shift;
495              
496             my @iter = ();
497             if ( exists( $self->{routes} ) ) {
498             for my $rte ( @{ $self->{routes} } ) {
499             push @iter, _iterate_points( $rte->{points} );
500             }
501             }
502              
503             return _iterate_iterators( @iter );
504              
505             }
506              
507             =head2 C
508              
509             Get an iterator that visits all the trackpoints in a C.
510              
511             =cut
512              
513             sub iterate_trackpoints {
514             my $self = shift;
515              
516             my @iter = ();
517             if ( exists( $self->{tracks} ) ) {
518             for my $trk ( @{ $self->{tracks} } ) {
519             if ( exists( $trk->{segments} ) ) {
520             for my $seg ( @{ $trk->{segments} } ) {
521             push @iter, _iterate_points( $seg->{points} );
522             }
523             }
524             }
525             }
526              
527             return _iterate_iterators( @iter );
528             }
529              
530             =head2 C
531              
532             Get an iterator that visits all the points in a C. For example
533              
534             my $iter = $gpx->iterate_points();
535             while ( my $pt = $iter->() ) {
536             print "Point: ", join( ', ', $pt->{lat}, $pt->{lon} ), "\n";
537             }
538              
539             =cut
540              
541             sub iterate_points {
542             my $self = shift;
543              
544             return _iterate_iterators(
545             $self->iterate_waypoints(),
546             $self->iterate_routepoints(),
547             $self->iterate_trackpoints()
548             );
549             }
550              
551             =head2 C
552              
553             Compute the bounding box of all the points in a C returning
554             the result as a hash reference. For example:
555              
556             my $gpx = Geo::Gpx->new( xml => $some_xml );
557             my $bounds = $gpx->bounds();
558              
559             returns a structure like this:
560              
561             $bounds = {
562             minlat => 57.120939,
563             minlon => -2.9839832,
564             maxlat => 57.781729,
565             maxlon => -1.230902
566             };
567              
568             C<$iterator> defaults to C<$self-Eiterate_points>.
569              
570             =cut
571              
572             sub bounds {
573             my ( $self, $iter ) = @_;
574             $iter ||= $self->iterate_points;
575              
576             my $bounds = {};
577              
578             while ( my $pt = $iter->() ) {
579             $bounds->{minlat} = $pt->{lat}
580             if !defined $bounds->{minlat} || $pt->{lat} < $bounds->{minlat};
581             $bounds->{maxlat} = $pt->{lat}
582             if !defined $bounds->{maxlat} || $pt->{lat} > $bounds->{maxlat};
583             $bounds->{minlon} = $pt->{lon}
584             if !defined $bounds->{minlon} || $pt->{lon} < $bounds->{minlon};
585             $bounds->{maxlon} = $pt->{lon}
586             if !defined $bounds->{maxlon} || $pt->{lon} > $bounds->{maxlon};
587             }
588              
589             return $bounds;
590             }
591              
592             sub _enc {
593             return encode_entities_numeric( $_[0] );
594             }
595              
596             sub _tag {
597             my $name = shift;
598             my $attr = shift || {};
599             my @tag = ( '<', $name );
600              
601             # Sort keys so the tests can depend on hash output order
602             for my $n ( sort keys %{$attr} ) {
603             my $v = $attr->{$n};
604             push @tag, ' ', $n, '="', _enc( $v ), '"';
605             }
606              
607             if ( @_ ) {
608             push @tag, '>', @_, '\n";
609             }
610             else {
611             push @tag, " />\n";
612             }
613              
614             return join( '', @tag );
615             }
616              
617             sub _xml {
618             my $self = shift;
619             my $name = shift;
620             my $value = shift;
621             my $name_map = shift || {};
622              
623             my $tag = $name_map->{$name} || $name;
624              
625             if ( blessed $value && $value->can( 'xml' ) ) {
626             # Handles legacy Gpx::Cache objects that can
627             # render themselves. Note that Gpx::Cache->xml
628             # adds the wrapper - so this won't
629             # work correctly for trkpt and rtept
630             return $value->xml( $name );
631             }
632             elsif ( defined( my $enc = $self->{encoder}->{$name} ) ) {
633             return $enc->( $name, $value );
634             }
635             elsif ( ref $value eq 'HASH' ) {
636             my $attr = {};
637             my @cont = ( "\n" );
638             my $as_attr = $AS_ATTR{$name};
639              
640             # Shallow copy so we can delete keys as we output them
641             my %v = %{$value};
642             for my $k ( @{ $KEY_ORDER{$name} || [] }, sort keys %v ) {
643             if ( defined( my $vv = delete $v{$k} ) ) {
644             if ( defined $as_attr && $k =~ $as_attr ) {
645             $attr->{$k} = $vv;
646             }
647             else {
648             push @cont, $self->_xml( $k, $vv, $name_map );
649             }
650             }
651             }
652              
653             return _tag( $tag, $attr, @cont );
654             }
655             elsif ( ref $value eq 'ARRAY' ) {
656             return join '',
657             map { $self->_xml( $tag, $_, $name_map ) } @{$value};
658             }
659             else {
660             return _tag( $tag, {}, _enc( $value ) );
661             }
662             }
663              
664             sub _cmp_ver {
665             my ( $v1, $v2 ) = @_;
666             my @v1 = split( /[.]/, $v1 );
667             my @v2 = split( /[.]/, $v2 );
668              
669             while ( @v1 && @v2 ) {
670             my $cmp = ( shift @v1 <=> shift @v2 );
671             return $cmp if $cmp;
672             }
673              
674             return @v1 <=> @v2;
675             }
676              
677             =head2 C
678              
679             Generate GPX XML.
680              
681             my $gpx10 = $gpx->xml( '1.0' );
682             my $gpx11 = $gpx->xml( '1.1' );
683              
684             If the version is omitted it defaults to the value of the C
685             attibute. Parsing a GPX document sets the version. If the C
686             attribute is unset defaults to 1.0.
687              
688             C version 0.10 used L to render each of the
689             points. L generates a number of hardwired values to suit the
690             original application of that module which aren't appropriate for general
691             purpose GPX manipulation. Legacy mode is triggered by passing a list of
692             L points to the constructor; this should probably be avoided
693             for new applications.
694              
695             =cut
696              
697             sub xml {
698             my $self = shift;
699             my $version = shift || $self->{version} || '1.0';
700              
701             my @ret = ();
702              
703             push @ret, qq{\n};
704              
705             $self->{encoder} = {
706             time => sub {
707             my ( $n, $v ) = @_;
708             return _tag( $n, {}, _enc( $self->{handler}->{time}->( $v ) ) );
709             },
710             keywords => sub {
711             my ( $n, $v ) = @_;
712             return _tag( $n, {}, _enc( join( ', ', @{$v} ) ) );
713             }
714             };
715              
716             # Limit to the latest version we know about
717             if ( _cmp_ver( $version, '1.1' ) >= 0 ) {
718             $version = '1.1';
719             }
720             else {
721              
722             # Modify encoder
723             $self->{encoder}->{link} = sub {
724             my ( $n, $v ) = @_;
725             my @v = ();
726             push @v, $self->_xml( 'url', $v->{href} )
727             if exists( $v->{href} );
728             push @v, $self->_xml( 'urlname', $v->{text} )
729             if exists( $v->{text} );
730             return join( '', @v );
731             };
732             $self->{encoder}->{email} = sub {
733             my ( $n, $v ) = @_;
734             if ( exists( $v->{id} ) && exists( $v->{domain} ) ) {
735             return _tag( 'email', {},
736             _enc( join( '@', $v->{id}, $v->{domain} ) ) );
737             }
738             else {
739             return '';
740             }
741             };
742             $self->{encoder}->{author} = sub {
743             my ( $n, $v ) = @_;
744             my @v = ();
745             push @v, _tag( 'author', {}, _enc( $v->{name} ) )
746             if exists( $v->{name} );
747             push @v, $self->_xml( 'email', $v->{email} )
748             if exists( $v->{email} );
749             return join( '', @v );
750             };
751             }
752              
753             # Turn version into path element
754             ( my $vpath = $version ) =~ s{[.]}{/}g;
755              
756             my $ns = "http://www.topografix.com/GPX/$vpath";
757             my $schema = join( ' ', $ns, "$ns/gpx.xsd", @{ $self->{schema} } );
758              
759             push @ret, qq{
760             qq{xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" },
761             qq{version="$version" creator="Geo::Gpx" },
762             qq{xsi:schemaLocation="$schema" }, qq{xmlns="$ns">\n};
763              
764             my @meta = ();
765              
766             for my $fld ( @META ) {
767             if ( exists( $self->{$fld} ) ) {
768             push @meta, $self->_xml( $fld, $self->{$fld} );
769             }
770             }
771              
772             my $bounds = $self->bounds( $self->iterate_points() );
773             if ( %{$bounds} ) {
774             push @meta, _tag( 'bounds', $bounds );
775             }
776              
777             # Version 1.1 nests metadata in a metadata tag
778             if ( _cmp_ver( $version, '1.1' ) >= 0 ) {
779             push @ret, _tag( 'metadata', {}, "\n", @meta );
780             }
781             else {
782             push @ret, @meta;
783             }
784              
785             for my $k ( sort keys %XMLMAP ) {
786             if ( exists( $self->{$k} ) ) {
787             push @ret, $self->_xml( $k, $self->{$k}, $XMLMAP{$k} );
788             }
789             }
790              
791             push @ret, qq{\n};
792              
793             return join( '', @ret );
794             }
795              
796             =head2 C
797              
798             For compatability with L modules. Converts this object to a hash
799             with keys that correspond to the above methods. Generated ala:
800              
801             my %json = map { $_ => $self->$_ }
802             qw(name desc author keywords copyright
803             time link waypoints tracks routes version );
804             $json{bounds} = $self->bounds( $iter );
805              
806             With one difference: the keys will only be set if they are defined.
807              
808             =cut
809              
810             sub TO_JSON {
811             my $self = shift;
812             my %json; #= map {$_ => $self->$_} ...
813             for my $key ( @META, @ATTR ) {
814             my $val = $self->$key;
815             $json{$key} = $val if defined $val;
816             }
817             if ( my $bounds = $self->bounds ) {
818             $json{bounds} = $self->bounds;
819             }
820             return \%json;
821             }
822              
823             #### Legacy methods from 0.10
824              
825             =head2 C
826              
827             Synonym for C. Provided for compatibility with version 0.10.
828              
829             =cut
830              
831             sub gpx {
832             my $self = shift;
833             return $self->xml( @_ );
834             }
835              
836             =head2 C
837              
838             Provided for compatibility with version 0.10.
839              
840             =cut
841              
842             sub loc {
843             my $self = shift;
844             my @ret = ();
845             push @ret, qq{\n};
846             push @ret, qq{\n};
847              
848             if ( exists( $self->{waypoints} ) ) {
849             for my $wpt ( @{ $self->{waypoints} } ) {
850             push @ret, $wpt->loc();
851             }
852             }
853              
854             push @ret, qq{\n};
855              
856             return join( '', @ret );
857             }
858              
859             =head2 C
860              
861             Provided for compatibility with version 0.10.
862              
863             =cut
864              
865             sub gpsdrive {
866             my $self = shift;
867             my @ret = ();
868              
869             if ( exists( $self->{waypoints} ) ) {
870             for my $wpt ( @{ $self->{waypoints} } ) {
871             push @ret, $wpt->gpsdrive();
872             }
873             }
874              
875             return join( '', @ret );
876             }
877              
878             1;
879             __END__