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