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