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 5     5   26 use warnings;
  5         11  
  5         116  
3 5     5   20  
  5         7  
  5         176  
4             our $VERSION = '1.01';
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 5     5   2058 use Carp qw(confess croak cluck);
  5         15  
  5         358  
26 5     5   52 use Data::Dumper;
  5         10  
  5         372  
27 5     5   28 use overload '+' => \&merge;
  5         10  
  5         373  
28 5     5   30  
  5         9  
  5         49  
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 60     60 1 334 croak 'second argument must be a Trackpoint object' unless $previous_pt->isa('Geo::TCX::Trackpoint')
50 60 100       233 }
51 29 50       128 croak 'new() takes only one or two arguments' if @_;
52             my $class = ref($proto) || $proto;
53 60 50       193 my ($chomped_str, $t);
54 60   33     277 if ( $track_str =~ m,\s*^\<Track\>(.*)\</Track\>\s*$,gs ) {
55 60         125 $chomped_str = $1
56 60 50       785 } else { croak 'not a proper track string' }
57 60         956  
58 0         0 $t = {};
59             $t->{Points} = [];
60 60         150  
61 60         266 while ($chomped_str=~ m,(\<Trackpoint\>.*?\</Trackpoint\>),gs) {
62             my $pt = Geo::TCX::Trackpoint::Full->new($1, $previous_pt);
63 60         420 $previous_pt = $pt;
64 3481         10098 push @{$t->{Points}}, $pt
65 3481         4951 }
66 3481         4208 bless($t, $class);
  3481         24299  
67             return $t
68 60         241 }
69 60         373  
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 19 $y = $y->clone;
92 3 50       35 my %opts = @_; # option are as_is => boole and speed => value
93 3         11  
94 3         27 unless ($opts{as_is}) {
95 3         21 $opts{tolerance} ||= 50;
96              
97 3 100       16 my ($gap, $msg);
98 2   50     12 $gap = $x->trackpoint(-1)->distance_to( $y->trackpoint(1) );
99             $msg = 'distance between the two tracks to merge is ' . $gap . ' meters, which '
100 2         5 . 'is larger than the tolerance of ' . $opts{tolerance} . ' meters';
101 2         10 croak $msg if $gap > $opts{tolerance};
102              
103 2         38 #
104 2 50       10 # 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         4 $y->distance_net;
109 2         7 $y->distance_add( $dist_to_add );
110              
111 2         11 $y->trackpoint(1)->distance_elapsed( $gap, force => 1);
112 2         7  
113             #
114 2         7 # 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         5 $elapsed_t = sprintf '%.0f', $gap / $speed;
120 2         7  
121 2 100       12 $y->time_subtract( $duration );
122 2         9 $y->time_add( DateTime::Duration->new( seconds => $elapsed_t ));
123              
124 2         11 $y->trackpoint(1)->time_elapsed($elapsed_t, force => 1);
125 2         11  
126              
127 2         13 # 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         13 }
  3         23  
146 3         9 return $x
147 64         62 }
  64         92  
148              
149 3         26 =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 2141 my @slice1 = @ { $t1->{Points} } [0 .. $pt_no - 1];
169 10 50       35 my @slice2 = @ { $t1->{Points} } [$pt_no .. $n_pts- 1];
170 10         33 $t1->{Points} = \@slice1;
171 10         40 $t2->{Points} = \@slice2;
172 10         47 return $t1, $t2
  10         128  
173 10         63 }
  10         44  
174 10         45  
175 10         111 # 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         70  
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 104 my ($closest_pt, $min_dist, $pt_no) = $t->point_closest_to( $to_pt );
193 2 50       8 # here we can print some info about the original track and where it will be split
194 2 50 33     14 my ($t1, $t2) = $t->split( $pt_no );
195             return $t1, $t2
196 2 50       47 }
197 2         167  
198             =over 4
199 2         41  
200 2         23 =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 11 for my $i (1 .. $n_points) {
217 2         9 my $pt = $orig_t->trackpoint($n_points - $i + 1)->clone;
218 2         14  
219 2         20 if ($i == 1) {
220 2         9 $pt->_reset_distance( 0 );
221             $pt->_reset_time( $orig_t->trackpoint(1)->Time )
222 2         12 } else {
223 48         150 $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       103 }
226 2         12  
227 2         9 $previous_pt = $pt;
228             $previous_pt_orig = $orig_t->trackpoint($n_points - $i + 1)->clone;
229 46         251 # need copy of the original previous pt bcs elapsed fields of $pt got updated above
230 46         133 push @{$t->{Points}}, $pt
231             }
232             return $t
233 48         80 }
234 48         135  
235             =over 4
236 48         89  
  48         175  
237             =item clone()
238 2         11  
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 97  
255 50         507 =over 4
256 50 50       758  
257 50         278 =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 6476     6476 1 8574  
276 6476 50       9020 =item trackpoints( qw/ # # ... / )
277 6476 50 33     14163  
278 6476 100       8935 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 6476         12871  
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 166     166 1 340  
294 166         320 =over 4
295 166         324  
296 166 100       351 =item distance_add( $meters )
297 2         6  
298 2         5 =item distance_subtract( $meters )
299 164         1159  
300             =item distance_net()
301 166         746  
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 72 return 1
321 22         56 }
  22         131  
322 1544         2173  
323 1544         4343 my $t = shift;
324             my $tp1 = $t->trackpoint(1);
325 22         69 $t->distance_subtract( $tp1->DistanceMeters - $tp1->distance_elapsed );
326             return 1
327             }
328              
329 19     19 1 68 =over 4
330 19         114  
331 19         55 =item time_add( @duration )
332              
333             =item time_subtract( @duration )
334              
335 18     18 1 61 Perform L<DateTime> math on the timestamps of each trackpoint in the track by adding or subtracting the specified duration. Return true.
336 18         107  
337 18         287 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         103  
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 120 my $dur = shift;
357 9 100 66     63 $t->{Points}[$_]->time_subtract($dur) for (0 .. $#{$t->{Points}})
358 3         7 } else {
359 3         6 my @dur= @_;
  3         25  
360             $t->{Points}[$_]->time_subtract(@dur) for (0 .. $#{$t->{Points}})
361 6         21 }
362 6         13 return 1
  6         67  
363             }
364 9         43  
365             =over 4
366              
367             =item point_closest_to( $point or $trackpoint )
368 7     7 1 18  
369 7 100 66     58 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         7  
371 3         4 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         22  
372              
373 4         15 =back
374 4         8  
  4         37  
375             =cut
376 7         43  
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 20 next
395 4 50       13 }
396 4         10 my $distance = $gc->distance_to({ lat => $lat, lon => $lon });
397 4 50 33     64 $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         26 $min_dist = $distance;
402 4         1076 $pt_no = $_ + 1
403 4         8 }
  4         21  
404 250         563 }
405 250         1348 return ($closest_pt, $min_dist, $pt_no) if wantarray;
406 250         895 return $closest_pt
407 250 100 66     1062 }
408 1         211  
409             =over 4
410 1         6  
411 249         983 =item xml_string( # )
412 249   66     963502  
413 249   66     625 returns a string containing the XML representation of the object, equivalent to the string argument expected by C<new()>.
414 249 100       615  
415 125         149 =back
416 125         158  
417 125         217 =cut
418              
419             my $t = shift;
420 4 50       136 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 55 return $str
436 21         111 }
437              
438 21 100       121 =over 4
439 21 100       116  
440 21 100       130 =item summ()
441              
442 21         79 For debugging purposes, summarizes the fields of the track by printing them to screen. Returns true.
443              
444 21         41 =back
  21         91  
445              
446             =cut
447              
448 1238         2986 my $t = shift;
449             croak 'summ() expects no arguments' if @_;
450 21         76 my %fields;
451 21         618 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         3 =head1 VERSION
493 1         4  
494 1         2 1.01
495              
496             =head1 SEE ALSO
497              
498 1     1   3 perl(1).
499 1         3  
500 1         3 =cut
501              
502             1;
503