File Coverage

blib/lib/Geo/TCX/Lap.pm
Criterion Covered Total %
statement 222 252 88.1
branch 85 140 60.7
condition 4 6 66.6
subroutine 24 26 92.3
pod 15 15 100.0
total 350 439 79.7


line stmt bran cond sub pod time code
1             package Geo::TCX::Lap;
2 7     7   48 use strict;
  7         17  
  7         197  
3 7     7   32 use warnings;
  7         15  
  7         408  
4              
5             our $VERSION = '1.03';
6             our @ISA=qw(Geo::TCX::Track);
7              
8             =encoding utf-8
9              
10             =head1 NAME
11              
12             Geo::TCX::Lap - Extract and edit info from Lap data
13              
14             =head1 SYNOPSIS
15              
16             use Geo::TCX::Lap;
17              
18             =head1 DESCRIPTION
19              
20             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.
21              
22             A sub-class of L<Geo::TCX::Track>, it enables extracting and editing lap information associated with tracks contained in Garmin TCX files. Laps are a more specific form of a Track in that may contain additional information such as lap aggregates (e.g. TotalTimeSeconds, DistanceMeters, …), performance metrics (e.g. MaximumSpeed, AverageHeartRateBpm, …), and other useful fields.
23              
24             The are two types of C<Geo::TCX::Lap>: Activity and Courses.
25              
26             =over 4
27              
28             =item Activity
29              
30             Activity laps are tracks recorded by the Garmin from one of the activity types ('Biking', 'Running', 'MultiSport', 'Other') and saved in what is often refered to ashistory files.
31              
32             =item Course
33              
34             Course laps typically originate from history files that are converted to a course either by a Garmin device or some other software for the purpose of navigation or training. They contain course-specific fields such as C<BeginPosition> and C<EndPosition> and some lap aggregagates but do not contain the performance-metrics or other fields that acivity laps contain.
35              
36             =back
37              
38             See the AUTOLOAD section for a list of all supported fields for each type of lap.
39              
40             Some methods and accessors are applicable only to one type. This is specified in the documentation for each.
41              
42             =cut
43              
44 7     7   39 use Carp qw(confess croak cluck);
  7         14  
  7         515  
45 7     7   3181 use Geo::TCX::Track;
  7         24  
  7         336  
46 7     7   51 use overload '+' => \&merge;
  7         17  
  7         44  
47 7     7   469 use vars qw($AUTOLOAD %possible_attr);
  7         15  
  7         23059  
48              
49             # file-scoped lexicals
50             my @attr = qw/ AverageHeartRateBpm Cadence Calories DistanceMeters Intensity MaximumHeartRateBpm MaximumSpeed TotalTimeSeconds TriggerMethod StartTime BeginPosition EndPosition/;
51             $possible_attr{$_} = 1 for @attr;
52             # last 2 are specific to courses only
53             # no Track tag, wouldn't make sense to AUTOLOAD it
54              
55             =head2 Constructor Methods (class)
56              
57             =over 4
58              
59             =item new( $xml_string, $lapno )
60              
61             parses and xml string in the form of the lap portion from a Garmin Activity or Course and returns a C<Geo::TCX::Lap> object.
62              
63             No examples are provided as this constructor is typically called by instances of L<Geo::TCX>. The latter then provides various methods to access lap data and info. The I<$lapno> (lap number) is optional.
64              
65             =back
66              
67             =cut
68              
69             sub new {
70 55     55 1 158 my $proto = shift;
71 55   33     298 my $class = ref($proto) || $proto;
72 55         194 my ($str, $lapnumber, $last_point_previous_lap) = (shift, shift, shift);
73 55 100       199 if (ref $last_point_previous_lap) {
74 32 50       179 croak 'second argument must be a Trackpoint object'
75             unless $last_point_previous_lap->isa('Geo::TCX::Trackpoint')
76             }
77 55         145 my %opts = @_; # none for now, but setting it up
78              
79 55         157 my ($type, $starttime, $metrics, $metrics_and_track, $track_str);
80 55 100       5819 if ( $str =~ /\<Lap StartTime="(.*?)"\>(.*?)\<\/Lap\>/s ) {
    50          
81 45         132 $type = 'Activity';
82 45         146 $starttime = $1;
83 45         1385 $metrics_and_track = $2;
84 45 50       709 if ( $metrics_and_track =~ /(.*?)(\<Track\>.*\<\/Track\>)/s ) {
85 45         190 $metrics = $1;
86 45         987 $track_str = $2
87             }
88             } elsif ( $str =~ /\<Lap\>(.*?)\<\/Lap\>(.*)/s ) {
89 10         38 $type = 'Course';
90 10         148 $metrics = $1;
91 10         154 $track_str = $2
92 0         0 } else { croak 'string argument not in a format supported' }
93 55 50       219 croak 'No track data found in lap' unless $track_str;
94              
95             # First, create the track object from the super-class
96              
97 55         421 my $l = $class->SUPER::new( $track_str, $last_point_previous_lap );
98 55         190 bless($l, $class);
99              
100 55 100       224 if ($type eq 'Activity') {
101 45         453 $l->{_type} = 'Activity';
102 45         165 $l->{StartTime} = $starttime;
103              
104 45         268 $l->_process_remaining_lap_metrics( \$metrics );
105              
106             # Lap is smarter than Track:
107             # it knows that its StartTime may be ahead of the time of the first trackpoint
108             # so force a replace of the elapsed time with that time difference
109              
110             # StartTime is not a trackpoint, but we can create a fake one so we can
111             # get an trackpoint object that allows us to get the epoch time from it
112 45         266 my $fake = _fake_starttime_point( $l->{StartTime} );
113 45         299 my $time_elapsed = $l->trackpoint(1)->time_epoch - $fake->time_epoch;
114 45         196 $l->trackpoint(1)->time_elapsed( $time_elapsed, force => 1)
115             }
116 55 100       274 if ($type eq 'Course') {
117 10         109 $l->{_type} = 'Course';
118              
119             # Lap is again smarter than Track:
120             # but instead of knowing *when* it started (as for activities), it knows *where*
121             # nb: courses converted by save_laps() and Ride with GPS always have the BeginPosition
122             # equal to the first trackpoint.
123              
124 10 50       158 if ( $metrics =~ s,\<BeginPosition\>(.*)\</BeginPosition\>,,g) {
125 10         67 $l->{BeginPosition} = Geo::TCX::Trackpoint->new( $1 )
126             }
127 10 50       119 if ( $metrics =~ s,\<EndPosition\>(.*)\</EndPosition\>,,g) {
128 10         46 $l->{EndPosition} = Geo::TCX::Trackpoint->new( $1 )
129             }
130              
131 10         72 $l->_process_remaining_lap_metrics( \$metrics );
132              
133 10         49 my ($meters, $time_elapsed) = (undef, 0);
134             # can compare if $meters is almost identical to $l->trackpoint(1)->DistanceMeters;
135             # we could simply have used the later to estimate the time elapsed but it is nice
136             # to check from the BeginPosition
137 10         89 $meters = $l->{BeginPosition}->distance_to( $l->trackpoint(1) );
138 10 50       191 if ($meters > 0) {
139 0         0 my $avg_speed = $l->_avg_speed_meters_per_second;
140 0         0 $time_elapsed = sprintf( '%.0f', $meters / $avg_speed );
141             }
142 10         95 $l->trackpoint(1)->time_elapsed( $time_elapsed, force => 1 )
143             }
144              
145 55         208 $l->{_lapmetrics} = $metrics; # delete this ine once I am sure that I capture all metrics and track info properly
146              
147             # estimate auto-pause time for use by split()
148 55         330 $l->{_time_auto_paused} = sprintf( '%.2f', $l->totaltimeseconds - $l->TotalTimeSeconds);
149 55         379 return $l
150             }
151              
152             =head2 Constructor Methods (object)
153              
154             =over 4
155              
156             =item merge( $lap, as_is => boolean )
157              
158             Returns a new C<Geo::TCX::Lap> merged with the lap specified in I<$lap>.
159              
160             $merged = $lap1->merge( $lap2 );
161              
162             Adjustments for the C<DistanceMeters> and C<Time> fields of each trackpoint in the lap are made unless C<as_is> is set to true.
163              
164             Lap aggregates C<TotalTimeSeconds> and C<DistanceMeters> are adjusted. For Activity laps, performance metrics such as C<MaximumSpeed>, C<AverageHeartRateBpm>, …, are also adjusted. For Course laps, C<EndPosition> is also adjusted.
165              
166             Unlike the C<merge_laps()> method in L<Geo::TCX>, the laps do not need to originate from the same *.tcx file, hence there is also no requirement that they be consecutive laps as is the case in the former.
167              
168             =back
169              
170             =cut
171              
172             sub merge {
173 2     2 1 10 my ($x, $y) = (shift, shift);
174 2 50       16 croak 'both operands must be Lap objects' unless $y->isa('Geo::TCX::Lap');
175 2         9 my %opts = @_;
176              
177 2         12 my $m = $x->SUPER::merge($y, speed => $y->_avg_speed_meters_per_second, as_is => $opts{'as_is'});
178              
179 2         31 $m->{DistanceMeters} = $m->DistanceMeters + $y->DistanceMeters;
180 2         23 $m->{_time_auto_paused} = sprintf('%.2f', $m->{_time_auto_paused} + $y->{_time_auto_paused});
181              
182 2 100       9 if ($opts{as_is}) { # then do not adjust TTS, just summ them up
183 1         6 $m->{TotalTimeSeconds} = sprintf('%.2f', $m->TotalTimeSeconds + $y->TotalTimeSeconds)
184             } else {
185             # i.e. if the 2nd lap did not come from the same ride, we will have estimated the elapsed time bewteen the two tracks
186             $m->{TotalTimeSeconds} = sprintf('%.2f', $m->totaltimeseconds - $m->{_time_auto_paused})
187 1         7 }
188              
189 2 50       12 if ($m->is_activity) { # aggregates specific to activities
190 2         11 my $pcent = $y->TotalTimeSeconds / $m->TotalTimeSeconds;
191              
192             # max values
193 2 50       10 if (defined $m->MaximumSpeed) {
194 2 50       11 if (defined $y->MaximumSpeed) {
195 2 50       11 $m->{MaximumSpeed} = ($m->MaximumSpeed > $y->MaximumSpeed) ? $m->MaximumSpeed : $y->MaximumSpeed
196 0         0 } else { $m->{MaximumSpeed} = undef }
197             }
198 2 50       13 if (defined $m->MaximumHeartRateBpm) {
199 2 50       9 if (defined $y->MaximumHeartRateBpm) {
200 2 50       10 $m->{MaximumHeartRateBpm} = ($m->MaximumHeartRateBpm > $y->MaximumHeartRateBpm) ? $m->MaximumHeartRateBpm : $y->MaximumHeartRateBpm
201 0         0 } else { $m->{MaximumHeartRateBpm} = undef }
202             }
203              
204             # average values
205 2 50       125 if (defined $m->AverageHeartRateBpm) {
206 2 50       9 if (defined $y->AverageHeartRateBpm) {
207 2         15 $m->{AverageHeartRateBpm} = sprintf '%.0f', ( (1 - $pcent) * $m->AverageHeartRateBpm + $pcent * $y->AverageHeartRateBpm )
208 0         0 } else { $m->{AverageHeartRateBpm} = undef }
209             }
210 2 50       12 if (defined $m->Cadence) {
211 0 0       0 if (defined $y->Cadence) {
212 0         0 $m->{Cadence} = sprintf '%.0f', ( (1 - $pcent) * $m->Cadence + $pcent * $y->Cadence )
213 0         0 } else { $m->{Cadence} = undef }
214             }
215              
216             # summed values
217 2 50       9 if (defined $m->Calories) {
218 2 50       9 if (defined $y->Calories) {
219 2         11 $m->{Calories} = $m->Calories + $y->Calories
220 0         0 } else { $m->{Calories} = undef }
221             }
222              
223             # keep values of first lap for other attr: Intensity, TriggerMethod, and StartTime
224             # Intensity: I have never seen another setting than Active
225             # TriggerMethod: I consider that one barely relevant
226              
227             } else { # aggregates specific to courses
228 0         0 $m->{EndPosition} = $y->trackpoint(-1)->to_basic
229             }
230 2         12 return $m
231             }
232              
233             =over 4
234              
235             =item split( # )
236              
237             Returns a 2-element array of C<Geo::TCX::Lap> objects with the first consisting of the lap up to and including point number I<#> and the second consisting of the all trackpoints after that point.
238              
239             ($lap1, $lap2) = $merged->split( 45 );
240              
241             Lap aggregates C<TotalTimeSeconds> and C<DistanceMeters> are recalculated, some small measurement error is to be expected due to the amount of time the device was an auto-pause.
242              
243             For Activity laps, the performance metrics C<MaximumSpeed>, C<MaximumHeartRateBpm>, C<AverageHeartRateBpm>, C<Cadence>, and C<Calories> are also recalculated for each lap (if they were defined). C<StartTime> is also adjusted for the second lap.
244              
245             For Course laps, C<BeginPosition> and C<EndPosition> are also adjusted.
246              
247             Will raise exception unless called in list context.
248              
249             =back
250              
251             =cut
252              
253             sub split {
254 5     5 1 15 my $lap = shift;
255 5 50       17 croak 'split() expects to be called in list context' unless wantarray;
256 5         37 my ($l1, $l2) = $lap->SUPER::split( shift );
257              
258 5 50       33 if ($lap->is_activity) {
259 5         31 $l2->{StartTime} = $l1->trackpoint(-1)->Time;
260 5         19 for my $l ($l1, $l2 ) {
261 10 50       47 $l->{MaximumSpeed} = $l->maximumspeed if defined $l->MaximumSpeed;
262 10 50       86 $l->{MaximumHeartRateBpm} = $l->maximumheartratebpm if defined $l->MaximumHeartRateBpm;
263 10 50       51 $l->{AverageHeartRateBpm} = $l->averageheartratebpm if defined $l->AverageHeartRateBpm;
264 10 50       51 $l->{Cadence} = $l->cadence if defined $l->Cadence;
265              
266 10         38 my $pcent = $l->trackpoints / $lap->trackpoints;
267 10         86 $l->{_time_auto_paused} = sprintf( '%.2f', $lap->{_time_auto_paused} * $pcent );
268 10         42 $l->{TotalTimeSeconds} = sprintf( '%.2f', $l->totaltimeseconds - $l->{_time_auto_paused});
269 10         40 $l->{DistanceMeters} = $l->distancemeters;
270              
271 10 50       68 $l->{Calories} = sprintf('%.0f', $lap->Calories * $pcent) if defined $l->Calories
272             }
273             } else {
274 0         0 $l1->{EndPosition} = $l1->trackpoint(-1)->to_basic;
275 0         0 $l2->{BeginPosition} = $l2->trackpoint( 1)->to_basic;
276 0         0 $l2->trackpoint(1)->distance_elapsed(0, force => 1 );
277 0         0 $l2->trackpoint(1)->time_elapsed( 0, force => 1 );
278 0         0 for my $l ($l1, $l2 ) {
279 0         0 my $pcent = $l->trackpoints / $lap->trackpoints;
280 0         0 $l->{_time_auto_paused} = sprintf( '%.2f', $lap->{_time_auto_paused} * $pcent );
281 0         0 $l->{TotalTimeSeconds} = sprintf( '%.2f', $l->totaltimeseconds - $l->{_time_auto_paused});
282 0         0 $l->{DistanceMeters} = $l->distancemeters
283             }
284             }
285 5         34 return $l1, $l2
286             }
287              
288             =over 4
289              
290             =item reverse( # )
291              
292             This method is allowed only for Courses and returns a clone of the lap object with the order of the trackpoints reversed.
293              
294             $reversed = $lap->reverse;
295              
296             When reversing a course, the time and distance information is set at 0 at the first trackpoint. Therefore, the lap aggregates (C<DistanceMeters>, C<TotalTimeSeconds>) may be smaller by a few seconds and meters compared to the original lap due to loss of elapsed time and distance information from the original lap's first point.
297              
298             =back
299              
300             =cut
301              
302             sub reverse {
303 1     1 1 14 my $l = shift->clone;
304 1 50       12 croak 'reverse() can only be used on Course laps' unless $l->is_course;
305              
306 1         11 $l = $l->SUPER::reverse;
307 1         11 $l->trackpoint(1)->time_elapsed( 0, force => 1);
308             # will always be 0 for a reversed lap because I never estimate time b/w
309             # the last point of a track and the EndPosition (would not make sense)
310 1         4 $l->{BeginPosition} = $l->trackpoint( 1)->to_basic;
311 1         6 $l->{EndPosition} = $l->trackpoint(-1)->to_basic;
312             # if we assign an existing trackpoint to Begin/EndPos, should we strip the non-positional info?
313             # we could get the xml_string from the trakcpoints and create a new point with just the <Position>...</Position> stuff.
314             # I think we should, think about it
315 1         8 $l->{DistanceMeters} = $l->distancemeters;
316 1         5 $l->{TotalTimeSeconds} = $l->totaltimeseconds;
317 1         6 return $l
318             }
319              
320             =head2 AUTOLOAD Methods
321              
322             =over 4
323              
324             =item I<field>( $value )
325              
326             Methods with respect to certain fields can be autoloaded and return the current or newly set value.
327              
328             Possible fields for Activity laps consist of: C<AverageHeartRateBpm>, C<Cadence>, C<Calories>, C<DistanceMeters>, C<Intensity>, C<MaximumHeartRateBpm>, C<MaximumSpeed>, C<TotalTimeSeconds>, C<TriggerMethod>, C<StartTime>.
329              
330             Course laps contain aggregates such as C<DistanceMeters>, C<TotalTimeSeconds> but not much else. They also contain C<BeginPosition> and C<EndPosition> which are exclusive to courses. They also contain C<Intensity> which almost always equal to 'Active'.
331              
332             Some fields may contain a value of 0, C<Calories> being one example. It is safer to check if a field is defined with C<< if (defined $lap->Calories) >> rather than C<< if ($lap->Calories) >>.
333              
334             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.
335              
336             =back
337              
338             =cut
339              
340             sub AUTOLOAD {
341 443     443   2554 my $self = shift;
342 443         677 my $attr = $AUTOLOAD;
343 443         1943 $attr =~ s/.*:://;
344 443 100       2539 return unless $attr =~ /[^A-Z]/; # skip DESTROY and all-cap methods
345 329 50       1003 croak "invalid attribute method: -> $attr()" unless $possible_attr{$attr};
346 329 50       691 $self->{$attr} = shift if @_;
347 329         2139 return $self->{$attr};
348             }
349              
350             =head2 Object Methods
351              
352             =over 4
353              
354             =item is_activity()
355              
356             =item is_course()
357              
358             True if the given lap is of the type indicated by the method, false otherwise.
359              
360             =back
361              
362             =cut
363              
364 21 100   21 1 179 sub is_activity { return (shift->StartTime) ? 1 : 0 }
365 32 100   32 1 181 sub is_course { return (shift->StartTime) ? 0 : 1 }
366              
367             =over 4
368              
369             =item time_add( @duration )
370              
371             =item time_subtract( @duration )
372              
373             Perform L<DateTime> math on the timestamps of each trackpoint in the lap by adding or subtracting the specified duration. Return true.
374              
375             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() >>.
376              
377             =back
378              
379             =cut
380              
381             sub time_add {
382 6     6 1 302 my $l = shift;
383 6         21 my @duration = @_;
384 6         44 $l->SUPER::time_add( @duration);
385              
386 6 50       32 if ($l->is_activity) {
387             # need to increment StartTime as well since not <=> Time of 1st point
388 6         35 my $fake = _fake_starttime_point( $l->{StartTime} );
389 6         30 $fake->time_add(@duration);
390 6         25 $l->{StartTime} = $fake->Time
391             }
392 6         42 return 1
393             }
394              
395             sub time_subtract {
396 6     6 1 139 my $l = shift;
397 6         22 my @duration = @_;
398 6         43 $l->SUPER::time_subtract( @duration);
399              
400 6 50       44 if ($l->is_activity) {
401             # need to increment StartTime as well since not <=> Time of 1st point
402 6         30 my $fake = _fake_starttime_point( $l->{StartTime} );
403 6         34 $fake->time_subtract(@duration);
404 6         33 $l->{StartTime} = $fake->Time
405             }
406 6         43 return 1
407             }
408              
409             sub _fake_starttime_point {
410 57     57   183 my $starttime = shift;
411 57         380 my $fake_pt = Geo::TCX::Trackpoint::Full->new("<Trackpoint><Time>$starttime</Time><Position><LatitudeDegrees>45.5</LatitudeDegrees><LongitudeDegrees>-72.5</LongitudeDegrees></Position><DistanceMeters>0</DistanceMeters></Trackpoint>");
412 57         201 return $fake_pt
413             }
414              
415             =over 4
416              
417             =item distancemeters()
418              
419             =item totaltimeseconds()
420              
421             =item maximumspeed()
422              
423             =item maximumheartratebpm()
424              
425             =item averageheartratebpm()
426              
427             =item cadence()
428              
429             Calculate and return the distance meters, totaltimeseconds, maximum speed (notionally corresponding to a lap's C<DistanceMeters> and C<TotalTimeSeconds> fields) from the elapsed data contained in each point of the lap's track. The heartrate information is calculated based on the C<HeartRateBpm> field of the trackpoints. The cadence is computed from the average cadence of all the trackpoints' C<Cadence> fields.
430              
431             The methods do not (yet) reset the fields of the lap yet. The two values may differ due to rounding, the fact that the Garmin recorded the aggregate field with miliseconds and some additional distance the garmin may have recorded between laps, etc. Any difference should be insignificant in relation to the measurement error introduced by the device itself.
432              
433             =back
434              
435             =cut
436              
437             sub distancemeters {
438 11     11 1 20 my $l = shift;
439 11 50       32 croak 'distancemeters() expects no arguments' if @_;
440 11         20 my $distancemeters = 0;
441 11         35 for my $i (1 .. $l->trackpoints) {
442 512         937 $distancemeters += $l->trackpoint($i)->distance_elapsed
443             }
444 11         37 return $distancemeters
445             }
446              
447             sub totaltimeseconds {
448 67     67 1 158 my $l = shift;
449 67 50       214 croak 'totaltimeseconds() expects no arguments' if @_;
450 67         131 my $totaltimeseconds = 0;
451 67         328 for my $i (1 .. $l->trackpoints) {
452 4142         7092 $totaltimeseconds += $l->trackpoint($i)->time_elapsed
453             }
454 67         632 return $totaltimeseconds
455             }
456              
457             sub maximumspeed {
458 10     10 1 20 my $l = shift;
459 10 50       29 croak 'maximumspeed() expects no arguments' if @_;
460 10         30 my ($max_speed, $speed) = (0);
461 10         35 for (1 .. $l->trackpoints) {
462 469         870 $speed = $l->trackpoint($_)->distance_elapsed / $l->trackpoint($_)->time_elapsed;
463 469 100       1066 $max_speed = $speed if $speed > $max_speed
464             }
465 10         115 return sprintf("%.3f", $max_speed )
466             }
467              
468             sub maximumheartratebpm {
469 10     10 1 22 my $l = shift;
470 10 50       31 croak 'maximumheartratebpm() expects no arguments' if @_;
471 10 50       37 croak 'lap has no heart rate information' unless $l->MaximumHeartRateBpm;
472 10         30 my ($max_hr, $hr) = (0);
473 10         37 for (1 .. $l->trackpoints) {
474 469         993 $hr = $l->trackpoint($_)->HeartRateBpm;
475 469 100       1320 $max_hr = $hr if $hr > $max_hr
476             }
477 10         45 return sprintf("%.0f", $max_hr)
478             }
479              
480             sub averageheartratebpm {
481 10     10 1 19 my $l = shift;
482 10 50       33 croak 'averageheartratebpm() expects no arguments' if @_;
483 10 50       34 croak 'lap has no heart rate information' unless $l->AverageHeartRateBpm;
484 10         32 my $n_points = $l->trackpoints;
485 10         18 my $sum_hr;
486 10         28 for (1 .. $n_points) {
487 469         1032 $sum_hr += $l->trackpoint($_)->HeartRateBpm
488             }
489 10         58 return sprintf("%.0f", $sum_hr / $n_points)
490             }
491              
492             sub cadence {
493 0     0 1 0 my $l = shift;
494 0 0       0 croak 'cadence() expects no arguments' if @_;
495 0 0       0 croak 'lap has no cadence information' unless $l->Cadence;
496 0         0 my $n_points = $l->trackpoints;
497 0         0 my $sum_cadence;
498 0         0 for (1 .. $n_points) {
499 0         0 $sum_cadence += $l->trackpoint($_)->Cadence
500             }
501 0         0 return sprintf("%.0f", $sum_cadence / $n_points)
502             }
503              
504             =over 4
505              
506             =item xml_string()
507              
508             returns a string containing the XML representation of object, useful for subsequent saving into an *.tcx file. The string is equivalent to the string argument expected by C<new()>.
509              
510             =back
511              
512             =cut
513              
514             sub xml_string {
515 20     20 1 59 my ($l, $as_course, $str, %opts);
516 20         44 $l = shift;
517 20         101 %opts = @_;
518 20 100 100     145 $as_course = 1 if $opts{course} or $l->is_course;
519              
520 20 100       98 my $newline = $opts{indent} ? "\n" : '';
521 20 100       73 my $tab = $opts{indent} ? ' ' : '';
522              
523 20 100       63 if ( $as_course ) {
524 11         48 $str .= $newline . $tab x 3 . "<Lap>"
525             } else {
526 9         55 $str .= $newline . $tab x 3 . "<Lap StartTime=\"" . $l->{StartTime} . "\">"
527             }
528              
529             # the lap meta data
530 20 50       141 $str .= $newline . $tab x 4 . "<TotalTimeSeconds>" . $l->{TotalTimeSeconds} . "</TotalTimeSeconds>" if $l->{TotalTimeSeconds};
531 20 50       135 $str .= $newline . $tab x 4 . "<DistanceMeters>" . $l->{DistanceMeters} . "</DistanceMeters>" if $l->{DistanceMeters};
532              
533 20 100       71 if ( $as_course ) {
534 11         32 my ($beg, $end, $beg_lat, $beg_lon, $end_lat, $end_lon);
535 11 100       48 if ($l->is_course) {
536 7         100 $beg_lat = $l->BeginPosition->LatitudeDegrees;
537 7         42 $beg_lon = $l->BeginPosition->LongitudeDegrees;
538 7         48 $end_lat = $l->EndPosition->LatitudeDegrees;
539 7         34 $end_lon = $l->EndPosition->LongitudeDegrees;
540             } else {
541 4         16 $beg_lat = $l->trackpoint( 1)->LatitudeDegrees;
542 4         19 $beg_lon = $l->trackpoint( 1)->LongitudeDegrees;
543 4         20 $end_lat = $l->trackpoint(-1)->LatitudeDegrees;
544 4         22 $end_lon = $l->trackpoint(-1)->LongitudeDegrees;
545             }
546 11         63 $str .= $newline . $tab x 4 . "<BeginPosition>";
547 11         51 $str .= $newline . $tab x 5 . "<LatitudeDegrees>$beg_lat</LatitudeDegrees>";
548 11         47 $str .= $newline . $tab x 5 . "<LongitudeDegrees>$beg_lon</LongitudeDegrees>";
549 11         39 $str .= $newline . $tab x 4 . "</BeginPosition>";
550 11         39 $str .= $newline . $tab x 4 . "<EndPosition>";
551 11         43 $str .= $newline . $tab x 5 . "<LatitudeDegrees>$end_lat</LatitudeDegrees>";
552 11         48 $str .= $newline . $tab x 5 . "<LongitudeDegrees>$end_lon</LongitudeDegrees>";
553 11         39 $str .= $newline . $tab x 4 . "</EndPosition>";
554 11 50       77 $str .= $newline . $tab x 4 . "<Intensity>" . $l->{Intensity} . "</Intensity>" if $l->{Intensity};
555 11         43 $str .= $newline . $tab x 3 . "</Lap>"
556             } else {
557 9 50       60 $str .= $newline . $tab x 4 . "<MaximumSpeed>" . $l->{MaximumSpeed} . "</MaximumSpeed>" if $l->{MaximumSpeed};
558 9 50       53 $str .= $newline . $tab x 4 . "<Calories>" . $l->{Calories} . "</Calories>" if $l->{Calories};
559 9 50       55 $str .= $newline . $tab x 4 . "<AverageHeartRateBpm><Value>" . $l->{AverageHeartRateBpm} . "</Value></AverageHeartRateBpm>" if $l->{AverageHeartRateBpm};
560 9 50       54 $str .= $newline . $tab x 4 . "<MaximumHeartRateBpm><Value>" . $l->{MaximumHeartRateBpm} . "</Value></MaximumHeartRateBpm>" if $l->{MaximumHeartRateBpm};
561 9 50       55 $str .= $newline . $tab x 4 . "<Intensity>" . $l->{Intensity} . "</Intensity>" if $l->{Intensity};
562 9 50       31 $str .= $newline . $tab x 4 . "<Cadence>" . $l->{Cadence} . "</Cadence>" if $l->{Cadence};
563 9 50       59 $str .= $newline . $tab x 4 . "<TriggerMethod>" . $l->{TriggerMethod} . "</TriggerMethod>" if $l->{TriggerMethod};
564             }
565              
566 20 100       75 my $n_tabs = ($as_course) ? 3 : 4; # <Track> for Activities have one more level of indentation compared to Courses
567              
568 20         146 $str .= $l->SUPER::xml_string( indent => $opts{indent}, n_tabs => $n_tabs );
569              
570 20 100       80 unless ($as_course) {
571 9         43 $str .= $newline . $tab x 3 . "</Lap>"
572             }
573 20         131 return $str
574             }
575              
576             =head2 Overloaded Methods
577              
578             =over 4
579              
580             =item +
581              
582             can concatenate two laps by issuing C<$lap = $lap1 + $lap2> on two Lap objects.
583              
584             =back
585              
586             =cut
587              
588             #
589             # internal methods
590              
591             sub _process_remaining_lap_metrics {
592 55     55   205 my ($self, $lap_metrics) = @_;
593             # Some fields are contained within <Value>#</Value> attr, don't need this
594             # will add those back before saving any files
595 55         561 $$lap_metrics =~ s,\<Value\>(.*?)\<\/Value\>,$1,g;
596 55         468 while ( $$lap_metrics =~ /\<(.*?)\>(.*?)\<.*?\>/sg ) {
597 366         1923 $self->{$1} = $2
598             }
599             }
600              
601             sub _avg_speed_meters_per_second {
602 2     2   6 my $self = shift;
603 2         20 return $self->DistanceMeters / $self->TotalTimeSeconds
604             }
605              
606             sub _avg_speed_km_per_hour {
607 0     0     my $self = shift;
608 0           return $self->_avg_speed_meters_per_second * 3600 / 1000
609             }
610              
611             =head1 EXAMPLES
612              
613             Coming soon.
614              
615             =head1 AUTHOR
616              
617             Patrick Joly
618              
619             =head1 VERSION
620              
621             1.03
622              
623             =head1 SEE ALSO
624              
625             perl(1).
626              
627             =cut
628              
629             1;
630