File Coverage

blib/lib/Geo/TCX/Trackpoint.pm
Criterion Covered Total %
statement 226 248 91.1
branch 83 124 66.9
condition 16 30 53.3
subroutine 37 40 92.5
pod 8 8 100.0
total 370 450 82.2


line stmt bran cond sub pod time code
1             package Geo::TCX::Trackpoint;
2 8     8   102658 use strict;
  8         26  
  8         223  
3 8     8   37 use warnings;
  8         15  
  8         323  
4              
5             our $VERSION = '1.03';
6              
7             =encoding utf-8
8              
9             =head1 NAME
10              
11             Geo::TCX::Trackpoint - Class to store and edit TCX trackpoints
12              
13             =head1 SYNOPSIS
14              
15             use Geo::TCX::Trackpoint;
16              
17             =head1 DESCRIPTION
18              
19             This package is mainly used by the L<Geo::TCX> module and serves little purpose on its own. The interface is documented mostly for the purpose of code maintainance.
20              
21             L<Geo::TCX::Trackpoint> provides a data structure for TCX trackpoints and provides accessor methods to read and edit trackpoint data.
22              
23             TCX trackpoints are different from GPX trackpoints in that they contain tags such as C<AltitudeMeters>, C<DistanceMeters>, C<HeartRateBpm>, C<Time>, and potentially C<Cadence>, C<SensorState>. Also the coordinates are tagged with longer-form fields as C<LatitudeDegrees>, C<LongitudeDegrees>.
24              
25             =cut
26              
27 8     8   4337 use Geo::Calc;
  8         30211657  
  8         421  
28 8     8   5028 use Geo::Gpx::Point;
  8         9340168  
  8         349  
29 8     8   81 use Carp qw(confess croak cluck);
  8         131  
  8         418  
30 8     8   55 use vars qw($AUTOLOAD %possible_attr);
  8         19  
  8         8140  
31              
32             # file-scoped lexicals
33             my @attr = qw/ LatitudeDegrees LongitudeDegrees /;
34             $possible_attr{$_} = 1 for @attr;
35              
36             =head2 Constructor Methods
37              
38             =over 4
39              
40             =item new( $xml_str )
41              
42             Takes an xml string argument containing coordinates contained within the C<Position> xml tag (optional) as recorded by Garmin Edge devices and returns a basic C<Geo::TCX::Trackpoint> object containing only coordinates.
43              
44             $str_basic = '<Position><LatitudeDegrees>45.304996</LatitudeDegrees><LongitudeDegrees>-72.637243</LongitudeDegrees></Position>';
45             $tp_basic = Geo::TCX::Trackpoint->new( $str_basic );
46              
47             =item Geo::TCX::Trackpoint::Full::new( $xml_str, $previous_pt )
48              
49             Takes an xml string argument in the form of a Garmin TCX trackpoint, as recorded by Garmin Edge devices, and returns a C<Geo::TCX::Trackpoint::Full> object containing fields that are supplementary to coordinates. See the list of fields in the AUTOLOAD section below.
50              
51             $str_full = '<Trackpoint><Time>2014-08-11T10:25:26Z</Time><Position><LatitudeDegrees>45.304996</LatitudeDegrees><LongitudeDegrees>-72.637243</LongitudeDegrees></Position><AltitudeMeters>211.082</AltitudeMeters><DistanceMeters>13.030</DistanceMeters><HeartRateBpm><Value>80</Value></HeartRateBpm></Trackpoint>';
52              
53             $tp_full = Geo::TCX::Trackpoint::Full->new( $str_full );
54              
55             I<$previous_pt> is optional and if specified will be interpreted as the previous trackpoint and be used to keep track of the distance and time that have elapsed since the latter. See the methods below to access these "elapsed" fields. If no previous trackpoint is provided, the elapsed time will remain undefined and the elapsed distance will set to the C<DistanceMeters> field of the trackpoint.
56              
57             =back
58              
59             =cut
60              
61             sub new {
62 4010     4010 1 11959 my ($proto, $pt_str) = (shift, shift);
63 4010 50       9513 croak 'too many arguments specified' if @_;
64 4010   33     11678 my $class = ref($proto) || $proto;
65 4010         6872 my $pt = {};
66 4010         7168 bless($pt, $class);
67              
68             # Lat and Long are contained in that tag, not needed
69 4010         21853 $pt_str =~ s,\</*Position\>,,g;
70              
71             # initialize fields/attr
72 4010         21773 while ($pt_str =~ m,\<([^<>]*)\>(.*?)\</([^<>]*)\>,gs) {
73             # or could simply state =~ m,\<(.*?)\>(.*?)\</.*?\>,gs)
74 8020 50       22707 croak 'Could not match identical attr' unless $1 eq $3;
75 8020 50       18800 croak 'field not allowed' unless $possible_attr{$1};
76 8020         35195 $pt->{$1} = $2
77             }
78 4010         10269 return $pt
79             }
80              
81             =over 4
82              
83             =item clone()
84              
85             Returns a deep copy of a C<Geo::TCX::Trackpoint> instance.
86              
87             $clone = $trackpoint->clone;
88              
89             =back
90              
91             =cut
92              
93             sub clone {
94 98     98 1 5056 my $clone;
95 98         460 eval(Data::Dumper->Dump([ shift ], ['$clone']));
96 98 50       592 confess $@ if $@;
97 98         528 return $clone
98             }
99              
100             =head2 AUTOLOAD Methods
101              
102             =cut
103              
104             =over 4
105              
106             =item I<field>( $value )
107              
108             Methods with respect to certain fields can be autoloaded and return the current or newly set value.
109              
110             For Basic trackpoints, LatitudeDegrees and LongitudeDegrees are the supported fields.
111              
112             For Full trackpoints, supported fields are: LatitudeDegrees, LongitudeDegrees, AltitudeMeters, HeartRateBpm, Cadence, and SensorState.
113              
114             Some fields may contain a value of 0. It is safer to check if a field is defined with C<< if (defined $trackpoint->Cadence) >> rather than C<< if ($trackpoint->Cadence) >>.
115              
116             Caution should be used if setting a I<$value> as no checks are performed to ensure the value is appropriate or in the proper format.
117              
118             =back
119              
120             =cut
121              
122             sub AUTOLOAD {
123 106     106   210 my $self = shift;
124 106         176 my $attr = $AUTOLOAD;
125 106         406 $attr =~ s/.*:://;
126 106 100       832 return unless $attr =~ /[^A-Z]/; # skip DESTROY and all-cap methods
127 66 50       186 croak "invalid attribute method: -> $attr()" unless $possible_attr{$attr};
128 66 100       156 $self->{$attr} = shift if @_;
129 66         674 return $self->{$attr}
130             }
131              
132             =head2 Object Methods
133              
134             =cut
135              
136             =over 4
137              
138             =item to_gpx()
139              
140             Returns a trackpoint as a L<Geo::Gpx::Point>.
141              
142             =back
143              
144             =cut
145              
146             sub to_gpx {
147 1     1 1 4 my ($pt, %attr) = @_; # call to new() will handle error check
148 1         9 my %fields = ( lat => $pt->LatitudeDegrees, lon => $pt->LongitudeDegrees );
149 1 50       5 $fields{ele} = $pt->AltitudeMeters if defined $pt->AltitudeMeters;
150 1 50       4 $fields{time} = $pt->{_time_epoch} if defined $pt->Time;
151 1         14 return Geo::Gpx::Point->new( %fields, %attr );
152             }
153              
154             =over 4
155              
156             =item to_geocalc()
157              
158             Returns a trackpoint as a L<Geo::Calc> object.
159              
160             =back
161              
162             =cut
163              
164             sub to_geocalc {
165 1     1 1 1398 my $pt = shift;
166 1 50       5 croak "to_geocalc() takes no arguments" if @_;
167 1         7 return Geo::Calc->new( lat => $pt->LatitudeDegrees, lon => $pt->LongitudeDegrees );
168             }
169              
170             =over 4
171              
172             =item to_basic()
173              
174             Returns a trackpoint as a C<Geo::TCX::Trackpoint> object with only position information (i.e coordinates).
175              
176             =back
177              
178             =cut
179              
180             sub to_basic {
181 3     3 1 716 my $pt = shift;
182 3 50       14 croak "to_geocalc() takes no arguments" if @_;
183 3         10 my $newpt = {};
184 3         10 bless($newpt, 'Geo::TCX::Trackpoint');
185 3         18 $newpt->LatitudeDegrees( $pt->LatitudeDegrees );
186 3         13 $newpt->LongitudeDegrees( $pt->LongitudeDegrees );
187 3         15 return $newpt
188             }
189              
190             =over 4
191              
192             =item distance_to ( $trackpoint )
193              
194             Calculates and returns the distance to the specified I<$trackpoint> object using the L<Geo::Calc> module.
195              
196             =back
197              
198             =cut
199              
200             sub distance_to {
201 15     15 1 612 my ($from, $to) = (shift, shift);
202 15 50 33     173 croak 'expects a single trackpoint as argument' if @_ or ! $to->isa('Geo::TCX::Trackpoint');
203 15         174 my $g = Geo::Calc->new( lat => $from->LatitudeDegrees, lon => $from->LongitudeDegrees );
204 15         3118 my $dist = $g->distance_to( { lat => $to->LatitudeDegrees, lon => $to->LongitudeDegrees } );
205 15         208849 return $dist
206             }
207              
208             =over 4
209              
210             =item xml_string()
211              
212             returns a string containing the XML representation of the object, equivalent to the string argument expected by C<new()>.
213              
214             =back
215              
216             =cut
217              
218             sub xml_string {
219 0     0 1 0 my $pt = shift;
220 0         0 my %opts = @_;
221              
222 0 0       0 my $newline = $opts{indent} ? "\n" : '';
223 0 0       0 my $tab = $opts{indent} ? ' ' : '';
224 0 0       0 my $n_tabs = $opts{n_tabs} ? $opts{n_tabs} : 4;
225              
226 0         0 my $str;
227 0         0 $str .= $newline . $tab x ($n_tabs + 1) . '<Position>';
228 0         0 $str .= $newline . $tab x ($n_tabs + 2) . '<LatitudeDegrees>' . $pt->LatitudeDegrees . '</LatitudeDegrees>';
229 0         0 $str .= $newline . $tab x ($n_tabs + 2) . '<LongitudeDegrees>' . $pt->LongitudeDegrees . '</LongitudeDegrees>';
230 0         0 $str .= $newline . $tab x ($n_tabs + 1) . '</Position>';
231 0         0 return $str
232             }
233              
234             =over 4
235              
236             =item summ()
237              
238             For debugging purposes, summarizes the fields of the trackpoint by printing them to screen. Returns true.
239              
240             =back
241              
242             =cut
243              
244             sub summ {
245 0     0 1 0 my $pt = shift;
246 0 0       0 croak 'summ() expects no arguments' if @_;
247 0         0 my %fields;
248 0         0 foreach my $key (keys %{$pt}) {
  0         0  
249 0         0 print "$key: ", $pt->{$key}, "\n"
250             }
251 0         0 return 1
252             }
253              
254             package Geo::TCX::Trackpoint::Full;
255 8     8   73 use strict;
  8         21  
  8         268  
256 8     8   50 use warnings;
  8         19  
  8         403  
257              
258 8     8   56 use DateTime::Format::ISO8601;
  8         18  
  8         269  
259 8     8   48 use Carp qw(confess croak cluck);
  8         18  
  8         781  
260              
261             our $VERSION = '1.03';
262             our @ISA=qw(Geo::TCX::Trackpoint);
263              
264              
265             { # lexical scope for that package
266              
267 8     8   69 use vars qw($AUTOLOAD %possible_attr);
  8         21  
  8         19066  
268              
269             our ($LocalTZ, $Formatter);
270             $LocalTZ = DateTime::TimeZone->new( name => 'local' );
271             $Formatter = DateTime::Format::Strptime->new( pattern => '%a %b %e %H:%M:%S %Y' );
272             my $formatter_xsd = DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%SZ' );
273             # ... to avoid looking up timezone each time Trackpoint->new is called
274              
275             # file-scoped lexicals
276             my @attr = qw/ LatitudeDegrees LongitudeDegrees AltitudeMeters DistanceMeters Time HeartRateBpm Cadence SensorState /;
277             $possible_attr{$_} = 1 for @attr;
278              
279             sub new {
280 3980     3980   17760 my ($proto, $pt_str, $previous_pt) = (shift, shift, shift);
281 3980 100       10442 if (ref $previous_pt) {
282 3887 50       13973 croak 'second argument must be a Trackpoint object' unless $previous_pt->isa('Geo::TCX::Trackpoint')
283             }
284 3980 50       8842 croak 'too many arguments specified' if @_;
285 3980   33     14085 my $class = ref($proto) || $proto;
286              
287             # Ignoring Extensions tags, might support them at some point
288 3980         10027 $pt_str =~ s,\<Extensions\>.*?\</Extensions\>,,g;
289              
290 3980         5864 my $chomped_str = $pt_str;
291 3980 50       21918 if ( $chomped_str =~ m,\s*^\<Trackpoint\>(.*)\</Trackpoint\>\s*$,gs ) {
292 3980         11388 $chomped_str = $1
293             }
294             # contrary to Track, the <Trackpoint>...</Trackpoint> are optional
295              
296             # Extract the Position tag and create a basic positional trackpoint
297 3980         5797 my $pt;
298 3980 100       28470 if ( $chomped_str =~ s/(<Position>.*<\/Position>)//g ) {
299 3979         11198 $pt =$class->SUPER::new( $1 )
300             } else {
301             # $DB::single=1;
302             # I put a debug flag here because I want to see instances where
303             # a trackpoint does not have coordinates and see how I should address those
304             # croak 'no <Position>...</Position> xml tag in string'
305             # call it anyway for now until I figure out how to handle those
306 1         3 $pt = {};
307 1         3 bless($pt, $class);
308             }
309 3980         25078 $chomped_str =~ s,\</*Value\>,,g; # HeartRateBpm value contained in that tag, not needed
310              
311             # initialize fields/attr
312 3980         16754 while ($chomped_str=~ m,\<([^<>]*)\>(.*?)\</([^<>]*)\>,gs) {
313             # or could simply state =~ m,\<(.*?)\>(.*?)\</.*?\>,gs)
314 15032 50       35565 croak 'Could not match identical attr' unless $1 eq $3;
315 15032 50       31722 croak 'field not allowed' unless $possible_attr{$1};
316 15032         63413 $pt->{$1} = $2
317             }
318              
319             # for debugging -- allow trackpoints with only coordinates but inspect them in debugger
320 3980 50       9212 $pt->{_noTime} = 1 unless defined $pt->{Time};
321 3980 50       7847 $pt->{_noDist} = 1 unless defined $pt->{DistanceMeters};
322 3980 50 33     14703 if ($pt->{_noTime} or $pt->{_noDist}) {
323             # commented out as I am building my databases, way too many files to parse to inspect them now, will uncomment when I am done parsing my databases
324             # $DB::single=1
325             }
326              
327 3980 50       14480 $pt->_reset_distance( $pt->{DistanceMeters}, $previous_pt ) unless $pt->{_noDist};
328 3980 50       8854 unless ($pt->{_noTime}) {
329 3980         6873 my $orig_time_string = $pt->{Time};
330 3980 50       13345 $pt->_reset_time( $pt->{Time}, $previous_pt ) unless $pt->{_noTime};
331             print "strange ISO time not equal to time string from TCX file for this trackpoint\n"
332 3980 50       11289 if $orig_time_string ne $pt->{_time_iso8601};
333             }
334 3980         12025 return $pt
335             }
336              
337             sub AUTOLOAD {
338 19567     19567   38790 my $self = shift;
339 19567         25276 my $attr = $AUTOLOAD;
340 19567         52659 $attr =~ s/.*:://;
341 19567 100       98282 return unless $attr =~ /[^A-Z]/; # skip DESTROY and all-cap methods
342 11217 50       21435 croak "invalid attribute method: -> $attr()" unless $possible_attr{$attr};
343 11217 50       18877 $self->{$attr} = shift if @_;
344 11217         27891 return $self->{$attr}
345             }
346              
347             =head2 Object Methods for class Geo::TXC::Trackpoint::Full
348              
349             =over 4
350              
351             =item DistanceMeters()
352              
353             Returns the C<DistanceMeters> field of a trackpoint.
354              
355             =back
356              
357             =cut
358              
359 10728     10728   29189 sub DistanceMeters { return shift->{DistanceMeters} }
360              
361             =over 4
362              
363             =item distance_elapsed( $value, force => true/false )
364              
365             Returns the elapsed distance (in meters) of a point as initially computed when the trackpoint was created. The value is never reset unless C<< force => 1 >> is specified.
366              
367             C<force> is needed internally by L<Geo::TCX::Lap>'s C<split()> and L<Geo::TCX::Track>'s <merge()> methods. Use with caution.
368              
369             =back
370              
371             =cut
372              
373             sub distance_elapsed {
374 1057     1057   1689 my ($pt, $value) = (shift, shift);
375 1057         1526 my %opts = @_;
376 1057 100       1898 if (defined $value) {
377 2 50       11 croak "need to specify option 'force => 1' to set a value" unless $opts{force};
378 2         13 $pt->{_distance_elapsed} = sprintf '%.3f', $value
379             }
380             return $pt->{_distance_elapsed}
381 1057         3158 }
382              
383             =over 4
384              
385             =item Time()
386              
387             Returns the C<Time> field of a trackpoint.
388              
389             =back
390              
391             =cut
392              
393 6120     6120   26587 sub Time { return shift->{Time} }
394              
395             =over 4
396              
397             =item time_dt ()
398              
399             =item time_datetime ()
400              
401             Return a L<DateTime> object corresponding to the time of a trackpoint.
402              
403             =back
404              
405             =cut
406              
407 0     0   0 sub time_dt { return DateTime::Format::ISO8601->parse_datetime( shift->Time ) }
408 4848     4848   10583 sub time_datetime { return DateTime::Format::ISO8601->parse_datetime( shift->Time ) }
409             # we never store a DateTime object but provide a method to create one
410              
411             =over 4
412              
413             =item time_local( $trackpoint )
414              
415             Returns the formatted local time of the trackpoint. The local time is always represented based on the locale of the system that calls this method, not that of where the trackpoint was recorded. It is not possible to know in which time zone a trackpoint was recorded at this stage.
416              
417             =back
418              
419             =cut
420              
421 2     2   9 sub time_local { return shift->{_time_local} }
422              
423             =over 4
424              
425             =item time_add( @duration )
426              
427             =item time_subtract( @duration )
428              
429             Perform L<DateTime> math on the timestamps of each lap's starttime and trackpoint by adding the specified time duration and return true.
430              
431             The duration can be provided as an actual L<DateTime::Duration> object or an array of arguments as per the syntax of L<DateTime>'s C<add()> or C<subtract()> methods, which expect a hash of keys such as
432             years => 3,
433             months => 5,
434             weeks => 1,
435             days => 1,
436             hours => 6,
437             minutes => 15,
438             seconds => 45,
439             nanoseconds => 12000,
440             end_of_month => 'limit'
441              
442             where only the relevant keys need to be specified i.e. C<< time_add( minutes > 30, seconds > 15) >>.
443              
444             =back
445              
446             =cut
447              
448             sub time_add {
449 436     436   1038 my ($pt, $dur) = shift;
450 436 100 66     1452 if (ref $_[0] and $_[0]->isa('DateTime::Duration') ) {
451 63         115 $dur = shift
452 373         1313 } else { $dur = DateTime::Duration->new( @_ ) }
453 436         38223 my $dt = $pt->time_datetime;
454 436         199890 $dt->add( $dur );
455 436         375553 $pt->_set_time_keys( $dt );
456 436         4139 return 1
457             }
458              
459             sub time_subtract {
460 423     423   936 my ($pt, $dur) = shift;
461 423 100 66     1406 if (ref $_[0] and $_[0]->isa('DateTime::Duration') ) {
462 63         107 $dur = shift
463 360         1213 } else { $dur = DateTime::Duration->new( @_ ) }
464 423         36724 my $dt = $pt->time_datetime;
465 423         193198 $dt->subtract( $dur );
466 423         416262 $pt->_set_time_keys( $dt );
467 423         4064 return 1
468             }
469              
470             =over 4
471              
472             =item time_epoch()
473              
474             Returns the epoch time of a point.
475              
476             =back
477              
478             =cut
479              
480 140     140   451 sub time_epoch { return shift->{_time_epoch} }
481              
482             =over 4
483              
484             =item time_elapsed( $value, force => true/false )
485              
486             Returns the elapsed time of a point as initially computed when the trackpoint was created. The value is never reset unless C<< force => 1 >> is specified.
487              
488             C<force> is needed internally by L<Geo::TCX::Lap>'s constructor, C<split()>, and C<reverse()> methods as well as L<Geo::TCX::Track>'s <reverse()>. Use with caution.
489              
490             =back
491              
492             =cut
493              
494             sub time_elapsed {
495 4724     4724   7648 my ($pt, $value) = (shift, shift);
496 4724         6623 my %opts = @_;
497 4724 100       8408 if (defined $value) {
498 58 50       208 croak "need to specify option 'force => 1' to set a value" unless $opts{force};
499 58         189 $pt->{_time_elapsed} = $value
500             }
501             return $pt->{_time_elapsed}
502 4724         11180 }
503              
504             =over 4
505              
506             =item time_duration( $datetime or $trackpoint or $string or $integer )
507              
508             Returns a L<DateTime::Duration> object containing the duration between the timestamps of two trackpoints. Consistent with the documentation for L<DateTime::Duration> the "duration is relative to the object from which I<$datetime> is subtracted". The duration will be positive if the timestamp of I<$datetime> occurs prior to the trackpoint, otherwise it will be negative.
509              
510             This method accepts four forms for the argument: a L<DateTime> object such as that returned by C<< $pt->time >>, an ISO8601 string such as that returned by C<< $pt->Time >>, a Trackpoint object, or an integer than can be interpreted as an epoch time.
511              
512             These duration objects are useful to pass to C<time_add()> or C<time_subtract>.
513              
514             =back
515              
516             =cut
517              
518             sub time_duration {
519 5     5   18 my $self = shift;
520 5         10 my ($dt, $datetime);
521             # first arg can time DateTime or trackpoint, and epoch time, or a time string
522 5 100       35 if (ref $_[0]) {
    100          
523 2 50       17 if ( $_[0]->isa('DateTime') ) {
524 0         0 $datetime = $_[0]
525             } else {
526 2 50       14 croak 'object as argument must be either a DateTime or a Trackpoint instance'
527             unless $_[0]->isa('Geo::TCX::Trackpoint');
528 2         11 $datetime = $_[0]->time_datetime
529             }
530             } elsif ($_[0] =~ /^(\d+)$/) {
531 1         8 $datetime = DateTime->from_epoch( epoch => $1 )
532             } else {
533 2         10 $datetime = DateTime::Format::ISO8601->parse_datetime( $_[0] )
534             }
535 5         2395 $dt = $self->time_datetime;
536              
537 5         2202 my $dur = $dt->subtract_datetime( $datetime );
538 5         1651 return $dur
539             }
540              
541             sub xml_string {
542 1238     1238   1778 my $pt = shift;
543 1238         2653 my %opts = @_;
544              
545 1238 100       2464 my $newline = $opts{indent} ? "\n" : '';
546 1238 100       2196 my $tab = $opts{indent} ? ' ' : '';
547 1238 50       2196 my $n_tabs = $opts{n_tabs} ? $opts{n_tabs} : 4;
548              
549 1238         1495 my $str;
550 1238         2529 $str .= $newline . $tab x $n_tabs . '<Trackpoint>';
551 1238         2665 $str .= $newline . $tab x ($n_tabs + 1) . '<Time>' . $pt->Time . '</Time>';
552 1238 100       3914 if (defined $pt->LatitudeDegrees) {
553 1237         2679 $str .= $newline . $tab x ($n_tabs + 1) . '<Position>';
554 1237         3928 $str .= $newline . $tab x ($n_tabs + 2) . '<LatitudeDegrees>' . $pt->LatitudeDegrees . '</LatitudeDegrees>';
555 1237         4444 $str .= $newline . $tab x ($n_tabs + 2) . '<LongitudeDegrees>' . $pt->LongitudeDegrees . '</LongitudeDegrees>';
556 1237         2809 $str .= $newline . $tab x ($n_tabs + 1) . '</Position>';
557             }
558 1238         4183 $str .= $newline . $tab x ($n_tabs + 1) . '<AltitudeMeters>'. $pt->AltitudeMeters . '</AltitudeMeters>';
559 1238         3179 $str .= $newline . $tab x ($n_tabs + 1) . '<DistanceMeters>'. $pt->DistanceMeters . '</DistanceMeters>';
560 1238 100       3929 if (defined $pt->HeartRateBpm) {
561 1023         2858 $str .= '<HeartRateBpm><Value>'. $pt->HeartRateBpm . '</Value></HeartRateBpm>'
562             }
563 1238 50       3815 if (defined $pt->Cadence) {
564 0         0 $str .= '<Cadence>'. $pt->Cadence . '</Cadence>'
565             }
566 1238 50       3769 if (defined $pt->SensorState) {
567 0         0 $str .= '<SensorState>'. $pt->SensorState . '</SensorState>'
568             }
569 1238         2601 $str .= $newline . $tab x $n_tabs . '</Trackpoint>';
570 1238         4214 return $str
571             }
572              
573             # Internal methods and functions
574              
575             sub _reset_time { # called by new() and by Track.pm
576 3982     3982   7426 my ($pt, $time, $previous_pt) = @_;
577 3982 100 66     18335 $previous_pt = pop if ref $_[-1] and $_[-1]->isa('Geo::TCX::Trackpoint');
578 3982         6995 delete $pt->{_time_elapsed}; # by design, immutable in _set_*
579 3982         9869 $pt->_set_time_keys($time, $previous_pt);
580 3982         7472 return 1
581             }
582              
583             sub _reset_time_from_epoch { # called by Track.pm
584 46     46   89 my ($pt, $epoch, $previous_pt) = @_;
585 46         164 my $dt = DateTime->from_epoch( epoch => $epoch );
586 46         12932 delete $pt->{_time_elapsed};
587 46         133 $pt->_set_time_keys( $dt, $previous_pt );
588 46         283 return 1
589             }
590              
591             sub _reset_distance { # called by new() and by Track.pm
592 4028     4028   8895 my ($pt, $distance, $previous_pt) = @_;
593 4028 100       8945 if (ref $previous_pt) {
594 3933 50       11626 croak 'second argument must be a Trackpoint object' unless $previous_pt->isa('Geo::TCX::Trackpoint')
595             }
596 4028         6534 delete $pt->{_distance_elapsed};
597 4028         10472 $pt->_set_distance_keys($distance, $previous_pt);
598 4028         6008 return 1
599             }
600              
601             # Expects a I<$time_string> in a format parseable by L<DateTime::Format::ISO8601>'s C<parse_datetime> constructor
602             # . sets the time-related fields for the trackpoint. Returns true.
603             # . if the _time_elapsed key for the point is not already defined and another trackpoint object is also provided,
604             # e.g. the previous trackpoint, it will also set it (as number of seconds since the timestamp of that previous point)
605             # . allows a DateTime obj as argument instead of $time which is required by methods that need to modify time so
606             # that we can update the keys to be consistent with the new time e.g. time_add(), time_subtract(), _reset_time_from_epoch()
607              
608             sub _set_time_keys {
609 4887     4887   10195 my ($pt, $time, $previous_pt) = (shift, shift);
610 4887 100 66     18679 $previous_pt = pop if ref $_[-1] and $_[-1]->isa('Geo::TCX::Trackpoint');
611              
612 4887         7350 my $dt;
613 4887 100 66     15160 if ( ref( $time ) and $time->isa('DateTime') ) {
614 905         1591 $dt = $time
615             } else {
616 3982         6512 $pt->{Time} = $time;
617 3982         7937 $dt = $pt->time_datetime
618             }
619              
620 4887         1830718 $pt->{Time} = _time_format($dt);
621 4887         1049394 $pt->{_time_iso8601} = _time_format($dt);
622 4887         1010861 $pt->{_time_local} = _time_format($dt, local => 1);
623 4887         1094122 $pt->{_time_epoch} = $dt->epoch;
624              
625 4887 100       45677 if ( ! exists $pt->{_time_elapsed} ) { # i.e. immutable here
626 4028 100       8421 if ( $previous_pt ) {
627             $pt->{_time_elapsed} = $pt->{_time_epoch} - $previous_pt->{_time_epoch}
628 3933         8814 } else { $pt->{_time_elapsed} = undef }
  95         370  
629             }
630 4887         20356 return 1
631             }
632              
633             sub _time_format {
634 14661     14661   24444 my $dt = shift;
635             # !! TODO: check that ref is not a Garmin Object (croack that function is not a class method)
636 14661         29304 my %opts = @_;
637 14661 100       33143 if ($opts{'local'}) {
638 4887         15047 $dt->set_formatter( $Formatter ); # see pattern in $Formatter
639 4887         218366 $dt->set_time_zone( $LocalTZ )
640             } else {
641 9774         25187 $dt->set_formatter( $formatter_xsd )
642             }
643 14661         508636 return $dt->stringify
644             }
645              
646             # Expects a decimal-number or integer and sets the C<DistanceMeters> field for the trackpoint and returns true
647             # . if the _distance_elapsed key for the point is not already defined and another trackpoint object is also provided,
648             # e.g. the previous trackpoint, it will also set it (number of meters from that previous point)
649              
650             sub _set_distance_keys {
651 5572     5572   10113 my ($pt, $meters, $previous_pt) = shift;
652 5572 100 66     20994 $previous_pt = pop if ref $_[-1] and $_[-1]->isa('Geo::TCX::Trackpoint');
653 5572         8534 $meters = shift;
654              
655 5572         7829 my $meters_formatted;
656 5572 50       40173 $meters_formatted = sprintf("%.3f", $meters) if defined $meters;
657              
658 5572         10447 $pt->{DistanceMeters} = $meters_formatted;
659              
660 5572 100       11154 if ( ! exists $pt->{_distance_elapsed} ) { # i.e. immutable here
661 4028 100       7811 if ( $previous_pt ) {
662 3933         8255 my $dist_elapsed = $pt->DistanceMeters - $previous_pt->DistanceMeters;
663 3933         19267 $pt->{_distance_elapsed} = sprintf("%.3f", $dist_elapsed)
664 95         278 } else { $pt->{_distance_elapsed} = $meters_formatted }
665             }
666 5572         10395 return 1
667             }
668              
669             }
670              
671             =head1 EXAMPLES
672              
673             Coming soon.
674              
675             =head1 AUTHOR
676              
677             Patrick Joly
678              
679             =head1 VERSION
680              
681             1.03
682              
683             =head1 SEE ALSO
684              
685             perl(1).
686              
687             =cut
688              
689             1;
690              
691             __END__
692              
693             A trackpoint string looks like:
694              
695             <Time>2014-08-11T10:55:26Z</Time><Position><LatitudeDegrees>45.293131</LatitudeDegrees><LongitudeDegrees>-72.650505</LongitudeDegrees></Position><AltitudeMeters>368.591</AltitudeMeters><DistanceMeters>3844.748</DistanceMeters><HeartRateBpm><Value>128</Value></HeartRateBpm>