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 6     6   36 use warnings;
  6         12  
  6         165  
3 6     6   39  
  6         13  
  6         340  
4             our $VERSION = '1.02';
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 6     6   33 use Geo::TCX::Track;
  6         11  
  6         484  
45 6     6   2524 use overload '+' => \&merge;
  6         19  
  6         290  
46 6     6   43 use vars qw($AUTOLOAD %possible_attr);
  6         14  
  6         40  
47 6     6   388  
  6         14  
  6         19105  
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 55     55 1 157 my ($str, $lapnumber, $last_point_previous_lap) = (shift, shift, shift);
71 55   33     328 if (ref $last_point_previous_lap) {
72 55         173 croak 'second argument must be a Trackpoint object'
73 55 100       188 unless $last_point_previous_lap->isa('Geo::TCX::Trackpoint')
74 32 50       165 }
75             my %opts = @_; # none for now, but setting it up
76              
77 55         152 my ($type, $starttime, $metrics, $metrics_and_track, $track_str);
78             if ( $str =~ /\<Lap StartTime="(.*?)"\>(.*?)\<\/Lap\>/s ) {
79 55         166 $type = 'Activity';
80 55 100       5720 $starttime = $1;
    50          
81 45         129 $metrics_and_track = $2;
82 45         147 if ( $metrics_and_track =~ /(.*?)(\<Track\>.*\<\/Track\>)/s ) {
83 45         1112 $metrics = $1;
84 45 50       750 $track_str = $2
85 45         153 }
86 45         995 } elsif ( $str =~ /\<Lap\>(.*?)\<\/Lap\>(.*)/s ) {
87             $type = 'Course';
88             $metrics = $1;
89 10         33 $track_str = $2
90 10         36 } else { croak 'string argument not in a format supported' }
91 10         97 croak 'No track data found in lap' unless $track_str;
92 0         0  
93 55 50       200 # 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 55         393  
98 55         168 if ($type eq 'Activity') {
99             $l->{_type} = 'Activity';
100 55 100       250 $l->{StartTime} = $starttime;
101 45         410  
102 45         123 $l->_process_remaining_lap_metrics( \$metrics );
103              
104 45         250 # 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 45         235 $l->trackpoint(1)->time_elapsed( $time_elapsed, force => 1)
113 45         294 }
114 45         146 if ($type eq 'Course') {
115             $l->{_type} = 'Course';
116 55 100       244  
117 10         109 # 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 10 50       151 }
125 10         72 if ( $metrics =~ s,\<EndPosition\>(.*)\</EndPosition\>,,g) {
126             $l->{EndPosition} = Geo::TCX::Trackpoint->new( $1 )
127 10 50       122 }
128 10         53  
129             $l->_process_remaining_lap_metrics( \$metrics );
130              
131 10         91 my ($meters, $time_elapsed) = (undef, 0);
132             # can compare if $meters is almost identical to $l->trackpoint(1)->DistanceMeters;
133 10         38 # 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 10         96 my $avg_speed = $l->_avg_speed_meters_per_second;
138 10 50       193 $time_elapsed = sprintf( '%.0f', $meters / $avg_speed );
139 0         0 }
140 0         0 $l->trackpoint(1)->time_elapsed( $time_elapsed, force => 1 )
141             }
142 10         67  
143             $l->{_lapmetrics} = $metrics; # delete this ine once I am sure that I capture all metrics and track info properly
144              
145 55         213 # estimate auto-pause time for use by split()
146             $l->{_time_auto_paused} = sprintf( '%.2f', $l->totaltimeseconds - $l->TotalTimeSeconds);
147             return $l
148 55         303 }
149 55         402  
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 8  
174 2 50       18 my $m = $x->SUPER::merge($y, speed => $y->_avg_speed_meters_per_second, as_is => $opts{'as_is'});
175 2         9  
176             $m->{DistanceMeters} = $m->DistanceMeters + $y->DistanceMeters;
177 2         13 $m->{_time_auto_paused} = sprintf('%.2f', $m->{_time_auto_paused} + $y->{_time_auto_paused});
178              
179 2         17 if ($opts{as_is}) { # then do not adjust TTS, just summ them up
180 2         26 $m->{TotalTimeSeconds} = sprintf('%.2f', $m->TotalTimeSeconds + $y->TotalTimeSeconds)
181             } else {
182 2 100       12 # 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         7 $m->{TotalTimeSeconds} = sprintf('%.2f', $m->totaltimeseconds - $m->{_time_auto_paused})
184             }
185              
186             if ($m->is_activity) { # aggregates specific to activities
187 1         7 my $pcent = $y->TotalTimeSeconds / $m->TotalTimeSeconds;
188              
189 2 50       10 # max values
190 2         12 if (defined $m->MaximumSpeed) {
191             if (defined $y->MaximumSpeed) {
192             $m->{MaximumSpeed} = ($m->MaximumSpeed > $y->MaximumSpeed) ? $m->MaximumSpeed : $y->MaximumSpeed
193 2 50       11 } else { $m->{MaximumSpeed} = undef }
194 2 50       10 }
195 2 50       11 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       12 } else { $m->{MaximumHeartRateBpm} = undef }
199 2 50       10 }
200 2 50       12  
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       12 } else { $m->{AverageHeartRateBpm} = undef }
206 2 50       11 }
207 2         14 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       19 } 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       12 } else { $m->{Calories} = undef }
218 2 50       10 }
219 2         13  
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         12 =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 14 if ($lap->is_activity) {
255 5 50       30 $l2->{StartTime} = $l1->trackpoint(-1)->Time;
256 5         58 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         21 $l->{Cadence} = $l->cadence if defined $l->Cadence;
261 10 50       75  
262 10 50       69 my $pcent = $l->trackpoints / $lap->trackpoints;
263 10 50       58 $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         34  
267 10         95 $l->{Calories} = sprintf('%.0f', $lap->Calories * $pcent) if defined $l->Calories
268 10         42 }
269 10         41 } else {
270             $l1->{EndPosition} = $l1->trackpoint(-1)->to_basic;
271 10 50       63 $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         27  
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 11 # will always be 0 for a reversed lap because I never estimate time b/w
304 1 50       7 # the last point of a track and the EndPosition (would not make sense)
305             $l->{BeginPosition} = $l->trackpoint( 1)->to_basic;
306 1         16 $l->{EndPosition} = $l->trackpoint(-1)->to_basic;
307 1         10 # 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         6 $l->{DistanceMeters} = $l->distancemeters;
311 1         6 $l->{TotalTimeSeconds} = $l->totaltimeseconds;
312             return $l
313             }
314              
315 1         11 =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 443     443   2331 return $self->{$attr};
342 443         654 }
343 443         1975  
344 443 100       2778 =head2 Object Methods
345 329 50       899  
346 329 50       679 =over 4
347 329         2361  
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 191  
365 32 100   32 1 195 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 336 }
383 6         49 return 1
384 6         46 }
385              
386 6 50       31 my $l = shift;
387             my @duration = @_;
388 6         29 $l->SUPER::time_subtract( @duration);
389 6         28  
390 6         29 if ($l->is_activity) {
391             # need to increment StartTime as well since not <=> Time of 1st point
392 6         42 my $fake = _fake_starttime_point( $l->{StartTime} );
393             $fake->time_subtract(@duration);
394             $l->{StartTime} = $fake->Time
395             }
396 6     6 1 120 return 1
397 6         22 }
398 6         43  
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         39 }
403 6         33  
404 6         25 =over 4
405              
406 6         45 =item distancemeters()
407              
408             =item totaltimeseconds()
409              
410 57     57   135 =item maximumspeed()
411 57         396  
412 57         196 =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 24 for my $i (1 .. $l->trackpoints) {
439 11 50       32 $totaltimeseconds += $l->trackpoint($i)->time_elapsed
440 11         21 }
441 11         36 return $totaltimeseconds
442 512         949 }
443              
444 11         43 my $l = shift;
445             croak 'maximumspeed() expects no arguments' if @_;
446             my ($max_speed, $speed) = (0);
447             for (1 .. $l->trackpoints) {
448 67     67 1 128 $speed = $l->trackpoint($_)->distance_elapsed / $l->trackpoint($_)->time_elapsed;
449 67 50       226 $max_speed = $speed if $speed > $max_speed
450 67         175 }
451 67         356 return sprintf("%.3f", $max_speed )
452 4142         6948 }
453              
454 67         685 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 22 for (1 .. $l->trackpoints) {
459 10 50       27 $hr = $l->trackpoint($_)->HeartRateBpm;
460 10         26 $max_hr = $hr if $hr > $max_hr
461 10         33 }
462 469         874 return sprintf("%.0f", $max_hr)
463 469 100       1052 }
464              
465 10         94 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 18 my $sum_hr;
470 10 50       28 for (1 .. $n_points) {
471 10 50       36 $sum_hr += $l->trackpoint($_)->HeartRateBpm
472 10         31 }
473 10         34 return sprintf("%.0f", $sum_hr / $n_points)
474 469         928 }
475 469 100       1191  
476             my $l = shift;
477 10         55 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 20 for (1 .. $n_points) {
482 10 50       29 $sum_cadence += $l->trackpoint($_)->Cadence
483 10 50       38 }
484 10         43 return sprintf("%.0f", $sum_cadence / $n_points)
485 10         18 }
486 10         26  
487 469         965 =over 4
488              
489 10         63 =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 72 if ( $as_course ) {
516 20         66 my ($beg, $end, $beg_lat, $beg_lon, $end_lat, $end_lon);
517 20         106 if ($l->is_course) {
518 20 100 100     152 $beg_lat = $l->BeginPosition->LatitudeDegrees;
519             $beg_lon = $l->BeginPosition->LongitudeDegrees;
520 20 100       100 $end_lat = $l->EndPosition->LatitudeDegrees;
521 20 100       96 $end_lon = $l->EndPosition->LongitudeDegrees;
522             } else {
523 20 100       65 $beg_lat = $l->trackpoint( 1)->LatitudeDegrees;
524 11         41 $beg_lon = $l->trackpoint( 1)->LongitudeDegrees;
525             $end_lat = $l->trackpoint(-1)->LatitudeDegrees;
526 9         50 $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       148 $str .= $newline . $tab x 5 . "<LongitudeDegrees>$beg_lon</LongitudeDegrees>";
531 20 50       152 $str .= $newline . $tab x 4 . "</BeginPosition>";
532             $str .= $newline . $tab x 4 . "<EndPosition>";
533 20 100       138 $str .= $newline . $tab x 5 . "<LatitudeDegrees>$end_lat</LatitudeDegrees>";
534 11         37 $str .= $newline . $tab x 5 . "<LongitudeDegrees>$end_lon</LongitudeDegrees>";
535 11 100       53 $str .= $newline . $tab x 4 . "</EndPosition>";
536 7         100 $str .= $newline . $tab x 4 . "<Intensity>" . $l->{Intensity} . "</Intensity>" if $l->{Intensity};
537 7         43 $str .= $newline . $tab x 3 . "</Lap>"
538 7         72 } else {
539 7         31 $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         20 $str .= $newline . $tab x 4 . "<AverageHeartRateBpm><Value>" . $l->{AverageHeartRateBpm} . "</Value></AverageHeartRateBpm>" if $l->{AverageHeartRateBpm};
542 4         17 $str .= $newline . $tab x 4 . "<MaximumHeartRateBpm><Value>" . $l->{MaximumHeartRateBpm} . "</Value></MaximumHeartRateBpm>" if $l->{MaximumHeartRateBpm};
543 4         20 $str .= $newline . $tab x 4 . "<Intensity>" . $l->{Intensity} . "</Intensity>" if $l->{Intensity};
544 4         21 $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         94 }
547 11         68  
548 11         40 my $n_tabs = ($as_course) ? 3 : 4; # <Track> for Activities have one more level of indentation compared to Courses
549 11         166  
550 11         43 $str .= $l->SUPER::xml_string( indent => $opts{indent}, n_tabs => $n_tabs );
551 11         45  
552 11         43 unless ($as_course) {
553 11         35 $str .= $newline . $tab x 3 . "</Lap>"
554 11 50       81 }
555 11         39 return $str
556             }
557 9 50       63  
558 9 50       58 =head2 Overloaded Methods
559 9 50       54  
560 9 50       49 =over 4
561 9 50       55  
562 9 50       31 =item +
563 9 50       57  
564             can concatenate two laps by issuing C<$lap = $lap1 + $lap2> on two Lap objects.
565              
566 20 100       70 =back
567              
568 20         146 =cut
569              
570 20 100       72 #
571 9         34 # internal methods
572              
573 20         143 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 55     55   168 Coming soon.
593              
594             =head1 AUTHOR
595 55         569  
596 55         449 Patrick Joly
597 366         1868  
598             =head1 VERSION
599              
600             1.02
601              
602 2     2   7 =head1 SEE ALSO
603 2         21  
604             perl(1).
605              
606             =cut
607 0     0      
608 0           1;
609