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
|
|
|
|
|
|
|
|