File Coverage

blib/lib/Geo/TCX/Track.pm
Criterion Covered Total %
statement 188 198 94.9
branch 41 58 70.6
condition 15 29 51.7
subroutine 23 24 95.8
pod 15 16 93.7
total 282 325 86.7


line stmt bran cond sub pod time code
1             package Geo::TCX::Track;
2 7     7   45 use strict;
  7         13  
  7         191  
3 7     7   35 use warnings;
  7         12  
  7         284  
4              
5             our $VERSION = '1.03';
6              
7             =encoding utf-8
8              
9             =head1 NAME
10              
11             Geo::TCX::Track - Class to store and edit a TCX track and its trackpoints
12              
13             =head1 SYNOPSIS
14              
15             use Geo::TCX::Track;
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::Track> provides a data structure for tracks in TCX files as well as methods to store, edit and obtain information from its trackpoints.
22              
23             =cut
24              
25 7     7   3370 use Geo::TCX::Trackpoint;
  7         21  
  7         359  
26 7     7   56 use Carp qw(confess croak cluck);
  7         13  
  7         369  
27 7     7   43 use Data::Dumper;
  7         20  
  7         386  
28 7     7   40 use overload '+' => \&merge;
  7         13  
  7         60  
29              
30             =head2 Constructor Methods (class)
31              
32             =over 4
33              
34             =item new( xml_string )
35              
36             takes an I<xml_string> in the form recorded by Garmin devices (and its TCX format) and returns a track object composed of various L<Geo::TCX::Trackpoint> objects.
37              
38             The string argument is expected to be flat i.e. no line breaks as per the example below.
39              
40             $xml_string = '<Track><Trackpoint><Time>2014-08-11T10:25:23Z</Time><Position><LatitudeDegrees>45.305054</LatitudeDegrees><LongitudeDegrees>-72.637287</LongitudeDegrees></Position><AltitudeMeters>210.963</AltitudeMeters><DistanceMeters>5.704</DistanceMeters><HeartRateBpm><Value>75</Value></HeartRateBpm></Trackpoint></Track>';
41              
42             $t = Geo::TCX::Track->new( $xml_string );
43              
44             =back
45              
46             =cut
47              
48             sub new {
49 66     66 1 425 my ($proto, $track_str, $previous_pt) = (shift, shift, shift);
50 66 100       310 if (ref $previous_pt) {
51 32 50       165 croak 'second argument must be a Trackpoint object' unless $previous_pt->isa('Geo::TCX::Trackpoint')
52             }
53 66 50       263 croak 'new() takes only one or two arguments' if @_;
54 66   33     386 my $class = ref($proto) || $proto;
55 66         173 my ($chomped_str, $t);
56 66 50       1014 if ( $track_str =~ m,\s*^\<Track\>(.*)\</Track\>\s*$,gs ) {
57 66         1252 $chomped_str = $1
58 0         0 } else { croak 'not a proper track string' }
59              
60 66         198 $t = {};
61 66         288 $t->{Points} = [];
62              
63 66         489 while ($chomped_str=~ m,(\<Trackpoint\>.*?\</Trackpoint\>),gs) {
64 3921         12483 my $pt = Geo::TCX::Trackpoint::Full->new($1, $previous_pt);
65 3921         6929 $previous_pt = $pt;
66 3921         5214 push @{$t->{Points}}, $pt
  3921         30886  
67             }
68 66         277 bless($t, $class);
69 66         425 return $t
70             }
71              
72             =head2 Constructor Methods (object)
73              
74             =over 4
75              
76             =item merge( $track, as_is => boolean, speed => value )
77              
78             Returns a new merged with the track specified in I<$track>.
79              
80             $merged = $track1->merge( $track2 );
81              
82             Adjustments for the C<DistanceMeters> and C<Time> fields of each trackpoint in the track are made unless C<as_is> is set to true.
83              
84             If a I<value> is passed to field C<speed>, that value will be used to ajust the time elapsed between the first point of the I<$track> and the last point of the track to be merged with. Otherwise the speed will be estimated based on the total distance and time elapsed of all the trackpoints in the I<$track>. C<speed> has not effect if C<as_is> is true.
85              
86             =back
87              
88             =cut
89              
90             sub merge {
91 3     3 1 19 my ($x, $y) = (shift, shift);
92 3 50       23 croak 'both operands must be Track objects' unless $y->isa('Geo::TCX::Track');
93 3         14 $x = $x->clone;
94 3         32 $y = $y->clone;
95 3         17 my %opts = @_; # option are as_is => boole and speed => value
96              
97 3 100       15 unless ($opts{as_is}) {
98 2   50     16 $opts{tolerance} ||= 50;
99              
100 2         5 my ($gap, $msg);
101 2         11 $gap = $x->trackpoint(-1)->distance_to( $y->trackpoint(1) );
102             $msg = 'distance between the two tracks to merge is ' . $gap . ' meters, which '
103 2         48 . 'is larger than the tolerance of ' . $opts{tolerance} . ' meters';
104 2 50       13 croak $msg if $gap > $opts{tolerance};
105              
106             #
107             # Distance: adjust DistanceMeters of all trackpoints, elapsed of just the 1st one
108 2         6 my $dist_to_add;
109 2         9 $dist_to_add = $x->trackpoint(-1)->DistanceMeters + $gap - $y->trackpoint(1)->distance_elapsed;
110              
111 2         13 $y->distance_net;
112 2         8 $y->distance_add( $dist_to_add );
113              
114 2         7 $y->trackpoint(1)->distance_elapsed( $gap, force => 1);
115              
116             #
117             # Time: adjust Time of all trackpoints, elapsed of just the 1st one
118              
119 2         6 my ($duration, $speed, $elapsed_t);
120 2         8 $duration = $y->trackpoint(1)->time_duration( $x->trackpoint(-1) );
121 2 100       16 $speed = $opts{speed} ? $opts{speed} : $y->_speed_meters_per_second;
122 2         11 $elapsed_t = sprintf '%.0f', $gap / $speed;
123              
124 2         12 $y->time_subtract( $duration );
125 2         14 $y->time_add( DateTime::Duration->new( seconds => $elapsed_t ));
126              
127 2         16 $y->trackpoint(1)->time_elapsed($elapsed_t, force => 1);
128              
129              
130             # my $epoch_gap = $x->trackpoint(-1)->time_epoch + $delay;
131             # my $delta_epoch = $epoch_gap - $y->trackpoint(1)->time_epoch;
132             #
133             # my $delta_dist = $x->trackpoint(1)->DistanceMeters;
134             # adjust DistanceMeters of x points, netting to 0 at point 1
135              
136             # now applying both the distance netting and delta_epoch to each y point
137             # for my $pt (@{$y->{Points}}) {
138             # my $epoch = $pt->time_epoch;
139             # $epoch += $delta_epoch;
140             # $pt->time_epoch( $epoch );
141             # $pt->DistanceMeters( $pt->DistanceMeters - $delta_dist );
142             # push @{$x->{Points}}, $pt
143             }
144              
145 3         8 my @points_to_merge = @{$y->{Points}};
  3         23  
146 3         10 for my $pt (@points_to_merge) {
147 64         75 push @{$x->{Points}}, $pt
  64         112  
148             }
149 3         28 return $x
150             }
151              
152             =over 4
153              
154             =item split( # )
155              
156             Returns a 2-element array of C<Geo::TCX::Track> objects with the first consisting of the track up to and including point number I<#> and the second consisting of the all trackpoints after that point.
157              
158             ($track1, $track2) = $merged->split( 45 );
159              
160             Will raise exception unless called in list context.
161              
162              
163             =back
164              
165             =cut
166              
167             sub split {
168 10     10 1 2863 my ($t, $pt_no) = @_;
169 10 50       35 croak 'split() expects to be called in list context' unless wantarray;
170 10         42 my $n_pts = $t->trackpoints;
171 10         47 my ($t1, $t2) = ($t->clone, $t->clone);
172 10         53 my @slice1 = @ { $t1->{Points} } [0 .. $pt_no - 1];
  10         157  
173 10         42 my @slice2 = @ { $t1->{Points} } [$pt_no .. $n_pts- 1];
  10         73  
174 10         54 $t1->{Points} = \@slice1;
175 10         127 $t2->{Points} = \@slice2;
176 10         80 return $t1, $t2
177             }
178              
179             # keep undocumented for now, serves little purpose unless Lap.pm would want to call it directly, which it does not at this time.
180              
181             # =over 4
182             #
183             # =item split_at_point_closest_to( $point or $trackpoint or $coord_str )
184             #
185             # Equivalent to C<split()> but splits at the trackpoint that lies closest to a given L<Geo::Gpx::Point>, L<Geo::TCX::Trackpoint>, or a string that can be interpreted as coordinates by C<< Geo::Gpx::Point->flex_coordinates >>.
186             #
187             # =back
188             #
189             # =cut
190              
191             sub split_at_point_closest_to {
192 2     2 0 127 my ($t, $to_pt) = (shift, shift);
193 2 50       11 croak 'split() expects to be called in list context' unless wantarray;
194 2 50 33     17 croak 'split_at_point_closest_to() expects a single argument' if ! defined $to_pt or @_;
195             # can leverage most of the checks that will be done by point_closest_to
196 2 50       22 $to_pt = Geo::Gpx::Point->flex_coordinates( \$to_pt ) unless ref $to_pt;
197 2         192 my ($closest_pt, $min_dist, $pt_no) = $t->point_closest_to( $to_pt );
198             # here we can print some info about the original track and where it will be split
199 2         50 my ($t1, $t2) = $t->split( $pt_no );
200 2         28 return $t1, $t2
201             }
202              
203             =over 4
204              
205             =item reverse()
206              
207             Returns a clone of a track with the order of the trackpoints reversed.
208              
209             $reversed = $track->reverse;
210              
211             =back
212              
213             =cut
214              
215             sub reverse {
216 2     2 1 11 my $orig_t = shift;
217 2         9 my $t = $orig_t->clone;
218 2         15 my $n_points = $t->trackpoints;
219 2         23 $t->{Points} = [];
220 2         15 my ($previous_pt, $previous_pt_orig);
221              
222 2         10 for my $i (1 .. $n_points) {
223 48         176 my $pt = $orig_t->trackpoint($n_points - $i + 1)->clone;
224              
225 48 100       142 if ($i == 1) {
226 2         29 $pt->_reset_distance( 0 );
227 2         8 $pt->_reset_time( $orig_t->trackpoint(1)->Time )
228             } else {
229 46         143 $pt->_reset_distance( $previous_pt->DistanceMeters + $previous_pt_orig->distance_elapsed, $previous_pt );
230 46         131 $pt->_reset_time_from_epoch( $previous_pt->time_epoch + $previous_pt_orig->time_elapsed, $previous_pt)
231             }
232              
233 48         94 $previous_pt = $pt;
234 48         148 $previous_pt_orig = $orig_t->trackpoint($n_points - $i + 1)->clone;
235             # need copy of the original previous pt bcs elapsed fields of $pt got updated above
236 48         105 push @{$t->{Points}}, $pt
  48         194  
237             }
238 2         13 return $t
239             }
240              
241             =over 4
242              
243             =item clone()
244              
245             Returns a deep copy of a C<Geo::TCX::Track> instance.
246              
247             $c = $track->clone;
248              
249             =back
250              
251             =cut
252              
253             sub clone {
254 50     50 1 113 my $clone;
255 50         483 eval(Data::Dumper->Dump([ shift ], ['$clone']));
256 50 50       681 confess $@ if $@;
257 50         249 return $clone
258             }
259              
260             =head2 Object Methods
261              
262             =over 4
263              
264             =item trackpoint( # )
265              
266             returns the trackpoint object corresponding to trackpoint number I<#> for the track.
267              
268             I<#> is 1-indexed but C<-1>, C<-2>, …, still refer to the last, second to last, …, points respectively.
269              
270             =back
271              
272             =cut
273              
274             sub trackpoint {
275 6886     6886 1 11328 my ($t, $point_i) = (shift, shift);
276 6886 50       12393 croak 'trackpoints are 1-indexed, point 0 does not exist' if $point_i eq 0;
277 6886 50 33     18660 croak 'requires a single integer as argument' if ! $point_i or @_;
278 6886 100       11572 $point_i-- if $point_i > 0; # 1-indexed but want -1 to still refer to last
279 6886         17164 return $t->{Points}[$point_i]
280             }
281              
282             =over 4
283              
284             =item trackpoints( qw/ # # ... / )
285              
286             returns an array of L<Geo::TCX::Trackpoint> objects for the number of points specified in list if specified, or all trackpoints if called without arguments.
287              
288             =back
289              
290             =cut
291              
292             sub trackpoints {
293 171     171 1 411 my ($t, @point_list) = @_;
294 171         378 my $points = $t->{Points};
295 171         281 my @points;
296 171 100       413 if (@point_list) {
297 2         7 map --$_, @point_list; # decrement to get array indices
298 2         7 @points = @$points[@point_list];
299 169         1197 } else { @points = @$points }
300             return @points
301 171         903 }
302              
303             =over 4
304              
305             =item distance_add( $meters )
306              
307             =item distance_subtract( $meters )
308              
309             =item distance_net()
310              
311             Add or subtract to the DistanceMeters field of all points in a Track. Does not impact any other fields of trackpoints. Return true.
312              
313             C<distance_net> is equivalent to C<< $t->distance_subtract( $t->trackpoint(1)->DistanceMeters - $t->trackpoint(1)->distance_elapsed ) >>.
314              
315             =back
316              
317             =cut
318              
319             sub distance_add {
320 22     22 1 72 my ($t, $meters) = (shift, shift);
321 22         47 for my $i (0 .. $#{$t->{Points}}) {
  22         101  
322 1544         2267 my $tp = $t->{Points}[$i];
323 1544         2862 $tp->_set_distance_keys( $tp->DistanceMeters + $meters )
324             }
325 22         53 return 1
326             }
327              
328             sub distance_subtract {
329 19     19 1 63 my ($t, $meters) = (shift, shift);
330 19         113 $t->distance_add( - $meters );
331 19         39 return 1
332             }
333              
334             sub distance_net {
335 18     18 1 51 my $t = shift;
336 18         77 my $tp1 = $t->trackpoint(1);
337 18         87 $t->distance_subtract( $tp1->DistanceMeters - $tp1->distance_elapsed );
338 18         56 return 1
339             }
340              
341             =over 4
342              
343             =item time_add( @duration )
344              
345             =item time_subtract( @duration )
346              
347             Perform L<DateTime> math on the timestamps of each trackpoint in the track by adding or subtracting the specified duration. Return true.
348              
349             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. See the pod for C<< Geo::TCX::Trackpoint->time_add() >>.
350              
351             =back
352              
353             =cut
354              
355             sub time_add {
356 9     9 1 124 my $t = shift;
357 9 100 66     112 if (ref $_[0] and $_[0]->isa('DateTime::Duration') ) {
358 3         11 my $dur = shift;
359 3         7 $t->{Points}[$_]->time_add($dur) for (0 .. $#{$t->{Points}})
  3         30  
360             } else {
361 6         31 my @dur= @_;
362 6         20 $t->{Points}[$_]->time_add(@dur) for (0 .. $#{$t->{Points}})
  6         46  
363             }
364 9         42 return 1
365             }
366              
367             sub time_subtract {
368 7     7 1 19 my $t = shift;
369 7 100 66     60 if (ref $_[0] and $_[0]->isa('DateTime::Duration') ) {
370 3         9 my $dur = shift;
371 3         7 $t->{Points}[$_]->time_subtract($dur) for (0 .. $#{$t->{Points}})
  3         26  
372             } else {
373 4         13 my @dur= @_;
374 4         11 $t->{Points}[$_]->time_subtract(@dur) for (0 .. $#{$t->{Points}})
  4         34  
375             }
376 7         38 return 1
377             }
378              
379             =over 4
380              
381             =item point_closest_to( $point or $trackpoint )
382              
383             Takes any L<Geo::Gpx::Point> or L<Geo::TCX::Trackpoint> and returns the trackpoint that is closest to it on the track.
384              
385             If called in list context, returns a three element array consisting of the trackpoint, the distance from the coordinate to the trackpoint (in meters), and the point number of that trackpoint in the track.
386              
387             =back
388              
389             =cut
390              
391             # ::Lap calls it by inheritance from Geo::TCX's split_at_point_closest_to()
392              
393             sub point_closest_to {
394 4     4 1 25 my ($t, $to_pt) = (shift, shift);
395 4 50       18 croak 'closest_to() expects a single argument' if @_;
396 4         14 my $class = ref( $to_pt );
397 4 50 33     66 unless ($class->isa('Geo::TCX::Trackpoint') or $class->isa('Geo::Gpx::Point')) {
398 0         0 croak 'point_closest_to() expects a Geo::TCX::Trackpoint of Geo::Gpx::Point as argument'
399             }
400              
401 4         24 my $gc = $to_pt->to_geocalc;
402 4         1134 my ($closest_pt, $min_dist, $pt_no);
403 4         13 for (0 .. $#{$t->{Points}}) {
  4         38  
404 250         759 my $pt = $t->{Points}[$_];
405 250         1682 my $lat = $pt->LatitudeDegrees;
406 250         1051 my $lon = $pt->LongitudeDegrees;
407 250 100 66     1276 if (!$lat or !$lon) {
408 1         339 print "point number ", ($_ + 1), " doesn't have coordinates\n";
409             next
410 1         7 }
411 249         1215 my $distance = $gc->distance_to({ lat => $lat, lon => $lon });
412 249   66     1182584 $min_dist ||= $distance; # the first iteration
413 249   66     754 $closest_pt ||= $pt; # the first iteration
414 249 100       800 if ($distance < $min_dist) {
415 125         209 $closest_pt = $pt;
416 125         178 $min_dist = $distance;
417 125         291 $pt_no = $_ + 1
418             }
419             }
420 4 50       134 return ($closest_pt, $min_dist, $pt_no) if wantarray;
421 0         0 return $closest_pt
422             }
423              
424             =over 4
425              
426             =item xml_string( # )
427              
428             returns a string containing the XML representation of the object, equivalent to the string argument expected by C<new()>.
429              
430             =back
431              
432             =cut
433              
434             sub xml_string {
435 21     21 1 54 my $t = shift;
436 21         104 my %opts = @_;
437              
438 21 100       95 my $newline = $opts{indent} ? "\n" : '';
439 21 100       82 my $tab = $opts{indent} ? ' ' : '';
440 21 100       80 my $n_tabs = $opts{n_tabs} ? $opts{n_tabs} : 3;
441              
442 21         72 my $str .= $newline . $tab x $n_tabs . '<Track>';
443             # here, create a accessor that lists how many points there are in the track and do for my $i (1.. # of trackpoints)
444 21         41 for my $pt (@{$t->{Points}}) {
  21         76  
445             # looks like I coded this to ignore points without a Position as point 2014-08-11T10:25:40Z
446             # look into this
447             # next unless ($pt->LatitudeDegrees);
448 1238         3512 $str .= $pt->xml_string( indent => $opts{indent}, n_tabs => ($n_tabs + 1) )
449             }
450 21         66 $str .= $newline . $tab x $n_tabs . '</Track>';
451 21         399 return $str
452             }
453              
454             =over 4
455              
456             =item summ()
457              
458             For debugging purposes, summarizes the fields of the track by printing them to screen. Returns true.
459              
460             =back
461              
462             =cut
463              
464             sub summ {
465 0     0 1 0 my $t = shift;
466 0 0       0 croak 'summ() expects no arguments' if @_;
467 0         0 my %fields;
468 0         0 foreach my $key (keys %{$t}) {
  0         0  
469 0         0 print "$key: ", $t->{$key}, "\n"
470             }
471 0         0 return 1
472             }
473              
474             =head2 Overloaded Methods
475              
476             =over 4
477              
478             =item +
479              
480             merge two tracks by calling C<$track = $track1 + $track2>.
481              
482             =back
483              
484             =cut
485              
486             #
487             # internal methods
488              
489             sub _speed_meters_per_second {
490 1     1   3 my $t = shift;
491 1         2 my ($distance, $speed);
492 1         4 $distance = $t->trackpoint(-1)->DistanceMeters - $t->trackpoint(1)->DistanceMeters + $t->trackpoint(1)->distance_elapsed;
493 1         5 $speed = $distance / $t->_totalseconds;
494 1         2 return $speed
495             }
496              
497             sub _totalseconds {
498 1     1   3 my ($t, $totalseconds) = (shift, 0);
499 1         4 $totalseconds += $t->trackpoint($_)->time_elapsed for (1 .. $t->trackpoints);
500 1         3 return $totalseconds
501             }
502              
503             =head1 EXAMPLES
504              
505             Coming soon.
506              
507             =head1 AUTHOR
508              
509             Patrick Joly
510              
511             =head1 VERSION
512              
513             1.03
514              
515             =head1 SEE ALSO
516              
517             perl(1).
518              
519             =cut
520              
521             1;
522