line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
use strict; |
2
|
6
|
|
|
6
|
|
592154
|
use warnings; |
|
6
|
|
|
|
|
65
|
|
|
6
|
|
|
|
|
170
|
|
3
|
6
|
|
|
6
|
|
32
|
|
|
6
|
|
|
|
|
10
|
|
|
6
|
|
|
|
|
284
|
|
4
|
|
|
|
|
|
|
our $VERSION = '1.02'; |
5
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
=encoding utf-8 |
7
|
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
=head1 NAME |
9
|
|
|
|
|
|
|
|
10
|
|
|
|
|
|
|
Geo::TCX - Parse and edit and TCX activity and course files from GPS training devices |
11
|
|
|
|
|
|
|
|
12
|
|
|
|
|
|
|
=head1 SYNOPSIS |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
use Geo::TCX; |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
=head1 DESCRIPTION |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
C<Geo::TCX> enables the parsing and editing of TCX activity and course files. TCX files follow an XML schema developed by Garmin and common to its GPS sports devices. Among other methods, the module enables laps from an activity to be saved as individual *.tcx files, split into separate laps based on a given point, merged, or converted to courses to plan a future activity. |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
The module supports files containing a single Activity or Course. Database files consisting of multiple activities or courses are not supported. |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
The documentation regarding TCX files in general uses the terms history and activity quite interchangeably, including in the user guides such as the one for the Garmin Edge device the author of this module is using. In C<Geo::TCX>, the terms Activity/Activities are used to refer to tracks recorded by a device (consistently with the XML mark-up) and Course/Courses refer to planned tracks meant to be followed during an activity (i.e. the term history is seldomly used). |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
=cut |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
use Geo::TCX::Lap; |
27
|
6
|
|
|
6
|
|
2898
|
use File::Basename; |
|
6
|
|
|
|
|
24
|
|
|
6
|
|
|
|
|
231
|
|
28
|
6
|
|
|
6
|
|
47
|
use Cwd qw(cwd abs_path); |
|
6
|
|
|
|
|
10
|
|
|
6
|
|
|
|
|
599
|
|
29
|
6
|
|
|
6
|
|
44
|
use Carp qw(confess croak cluck); |
|
6
|
|
|
|
|
13
|
|
|
6
|
|
|
|
|
275
|
|
30
|
6
|
|
|
6
|
|
33
|
|
|
6
|
|
|
|
|
18
|
|
|
6
|
|
|
|
|
29601
|
|
31
|
|
|
|
|
|
|
=head2 Constructor Methods (class) |
32
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
=over 4 |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
=item new( $filename or $str_ref, work_dir => $working_directory ) |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
loads and returns a new Geo::TCX instance using the I<$filename> supplied as first argument or a string reference equivalent to the xml tags of a *.tcx file. |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
$o = Geo::TCX->new('2022-08-11-10-27-15.tcx'); |
40
|
|
|
|
|
|
|
or |
41
|
|
|
|
|
|
|
$o = Geo::TCX->new( \'...'); |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
C<work_dir> or C<wd> for short can be set to specify where to save any working files (such as with the save_laps() method). The module never actually L<chdir>'s, it just keeps track of where the user wants to save files (and not have to type filenames with path each time), hence it is always defined. |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
The optional C<work_dir> (or C<wd> for short) specifies where to save any working files, such as with the save_laps() method. It can be supplied as a relative path or as an absolute path. If C<work_dir> is omitted, it is set based on the path of the I<$filename> supplied or the current working directory if the constructor is called with an XML string reference (see C<< set_wd() >> for more info). |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=back |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
=cut |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
my ($proto, $first_arg) = (shift, shift); |
52
|
|
|
|
|
|
|
my %opts = @_; |
53
|
23
|
|
|
23
|
1
|
14925
|
my $o = {}; |
54
|
23
|
|
|
|
|
105
|
my $class = ref($proto) || $proto; |
55
|
23
|
|
|
|
|
75
|
bless($o, $class); |
56
|
23
|
|
33
|
|
|
213
|
|
57
|
23
|
|
|
|
|
92
|
my $txt; |
58
|
|
|
|
|
|
|
if (ref( $first_arg ) eq 'SCALAR') { |
59
|
23
|
|
|
|
|
66
|
$txt = $$first_arg |
60
|
23
|
100
|
|
|
|
115
|
} else { |
61
|
5
|
|
|
|
|
13
|
croak 'first argument must be a filename' unless -f $first_arg; |
62
|
|
|
|
|
|
|
$txt = do { local(@ARGV, $/) = $first_arg; <> }; |
63
|
18
|
50
|
|
|
|
506
|
$o->set_filename($first_arg) |
64
|
18
|
|
|
|
|
71
|
} |
|
18
|
|
|
|
|
164
|
|
|
18
|
|
|
|
|
3479
|
|
65
|
18
|
|
|
|
|
169
|
|
66
|
|
|
|
|
|
|
$txt =~ s,\r,,g; # if it's a windows file |
67
|
|
|
|
|
|
|
$txt =~ s,>\s+<,><,g; |
68
|
23
|
|
|
|
|
186
|
$o->{tag_creator} = $1 if $txt =~ s/(<Creator.*<\/Creator>)//; |
69
|
23
|
|
|
|
|
9735
|
|
70
|
23
|
100
|
|
|
|
3109
|
# Activities/Activity - are as recorded by an EDGE 705 device |
71
|
|
|
|
|
|
|
# Courses/Course - are as converted by an EDGE 705 device from an Activity |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
$o->{tag_xml_version} = $1 if $txt =~ /(<.xml version[^>]*>)/; |
74
|
|
|
|
|
|
|
$o->{tag_trainingcenterdatabase} = $1 if $txt =~ /(<TrainingCenterDatabase.*<\/TrainingCenterDatabase>)/; |
75
|
23
|
50
|
|
|
|
321
|
$o->{tag_activities} = $1 if $txt =~ /(<Activities.*<\/Activities>)/; |
76
|
23
|
50
|
|
|
|
2822
|
$o->{tag_activity} = $1 if $txt =~ /(<Activity.*<\/Activity>)/; |
77
|
23
|
100
|
|
|
|
3188
|
$o->{tag_courses} = $1 if $txt =~ /(<Courses.*<\/Courses>)/; |
78
|
23
|
100
|
|
|
|
3058
|
$o->{tag_course} = $1 if $txt =~ /(<Course(?!s).*<\/Course>)/; |
79
|
23
|
100
|
|
|
|
1197
|
|
80
|
23
|
100
|
|
|
|
1295
|
# Id seems only for Activities/Activity... |
81
|
|
|
|
|
|
|
if ($o->{tag_activity}) { |
82
|
|
|
|
|
|
|
$o->{tag_id} = $1 if $o->{tag_activity} =~ /<Activity.*<Id>(.*)<\/Id>/; |
83
|
23
|
100
|
|
|
|
107
|
$o->{tag_activity_type} = $1 if $o->{tag_activity} =~ /<Activity Sport="([^"]+)"/; |
84
|
13
|
50
|
|
|
|
14690
|
} |
85
|
13
|
50
|
|
|
|
151
|
|
86
|
|
|
|
|
|
|
# ... and Name only for Courses/Course |
87
|
|
|
|
|
|
|
if ($o->{tag_course}) { |
88
|
|
|
|
|
|
|
# will pick up device name under Creator if we are not specific about the Course tag |
89
|
23
|
100
|
|
|
|
124
|
$o->{tag_name} = $1 if $o->{tag_course} =~ /<Course.*<Name>(.*)<\/Name>/ |
90
|
|
|
|
|
|
|
} |
91
|
10
|
50
|
|
|
|
1658
|
|
92
|
|
|
|
|
|
|
$o->{tag_author} = $1 if $txt =~ /(<Author.*<\/Author>)/; |
93
|
|
|
|
|
|
|
$o->_parse_author_tag if $o->{tag_author}; |
94
|
23
|
100
|
|
|
|
1820
|
|
95
|
23
|
100
|
|
|
|
191
|
my @Lap; |
96
|
|
|
|
|
|
|
if ( $o->{tag_activity} ) { |
97
|
23
|
|
|
|
|
64
|
my $i = 0; |
98
|
23
|
100
|
|
|
|
105
|
my $lap; |
99
|
13
|
|
|
|
|
40
|
while ( $o->{tag_activity} =~ /(\<Lap StartTime=.*?\>.*?\<\/Lap\>)/g ) { |
100
|
13
|
|
|
|
|
27
|
my ($lapstring, $last_point_previous_lap); |
101
|
13
|
|
|
|
|
2088
|
$lapstring = $1; |
102
|
45
|
|
|
|
|
134
|
$last_point_previous_lap = $lap->trackpoint(-1) if $i > 0; |
103
|
45
|
|
|
|
|
1541
|
$lap = Geo::TCX::Lap->new($lapstring, ++$i, $last_point_previous_lap); |
104
|
45
|
100
|
|
|
|
347
|
push @{ $o->{Laps} }, $lap |
105
|
45
|
|
|
|
|
397
|
} |
106
|
45
|
|
|
|
|
119
|
} |
|
45
|
|
|
|
|
4392
|
|
107
|
|
|
|
|
|
|
if ( $o->{tag_course} ) { |
108
|
|
|
|
|
|
|
# in Courses, data is structured as <Lap>...</Lap><Lap>...</Lap><Track>...</Track><Track>...</Track> |
109
|
23
|
100
|
|
|
|
125
|
# actually, not sure just seem like it's one long ... track, not multiple ones, which complicates things |
110
|
|
|
|
|
|
|
my $xml_str = $o->{tag_course}; |
111
|
|
|
|
|
|
|
|
112
|
10
|
|
|
|
|
36
|
my (@lap_tags, @lap_endpoints, @track_tags); |
113
|
|
|
|
|
|
|
|
114
|
10
|
|
|
|
|
25
|
if ( $xml_str =~ m,(<Lap>.*</Lap>),s ) { |
115
|
|
|
|
|
|
|
my $str = $1; |
116
|
10
|
50
|
|
|
|
724
|
@lap_tags = split(/(?s)<\/Lap>\s*<Lap>/, $str ); |
117
|
10
|
|
|
|
|
34
|
if (@lap_tags == 0) { push @lap_tags, $str } |
118
|
10
|
|
|
|
|
60
|
} |
119
|
10
|
50
|
|
|
|
58
|
|
|
0
|
|
|
|
|
0
|
|
120
|
|
|
|
|
|
|
for my $i (0 .. $#lap_tags) { |
121
|
|
|
|
|
|
|
my ($end_pos, $end_pt); |
122
|
10
|
|
|
|
|
60
|
if ( $lap_tags[$i] =~ m,<EndPosition>(.*)</EndPosition>,s ) { |
123
|
10
|
|
|
|
|
46
|
$end_pt = Geo::TCX::Trackpoint->new( $1 ); |
124
|
10
|
50
|
|
|
|
88
|
push @lap_endpoints, $end_pt |
125
|
10
|
|
|
|
|
180
|
} |
126
|
10
|
|
|
|
|
35
|
# since split removed tags sometimes at ^ of string for other at $ |
127
|
|
|
|
|
|
|
# let's remove them all and add back |
128
|
|
|
|
|
|
|
$lap_tags[$i] =~ s,</?Lap>,,g; |
129
|
|
|
|
|
|
|
$lap_tags[$i] =~ s,^,<Lap>,g; |
130
|
10
|
|
|
|
|
87
|
$lap_tags[$i] =~ s,$,</Lap>,g |
131
|
10
|
|
|
|
|
60
|
} |
132
|
10
|
|
|
|
|
76
|
my $track_str; |
133
|
|
|
|
|
|
|
if ( $xml_str =~ m,(<Track>.*</Track>),s ) { |
134
|
10
|
|
|
|
|
26
|
$track_str = $1; |
135
|
10
|
50
|
|
|
|
177
|
} |
136
|
10
|
|
|
|
|
106
|
|
137
|
|
|
|
|
|
|
my $t = Geo::TCX::Track->new( $track_str ); |
138
|
|
|
|
|
|
|
if (@lap_tags ==1) { $track_tags[0] = $track_str } |
139
|
10
|
|
|
|
|
81
|
else { |
140
|
10
|
50
|
|
|
|
64
|
my ($t1, $t2); |
|
10
|
|
|
|
|
37
|
|
141
|
|
|
|
|
|
|
for my $i (0 .. $#lap_tags ) { |
142
|
0
|
|
|
|
|
0
|
if ($i < $#lap_tags) { |
143
|
0
|
|
|
|
|
0
|
($t1, $t2) = $t->split_at_point_closest_to( $lap_endpoints[$i] ); |
144
|
0
|
0
|
|
|
|
0
|
push @track_tags, $t1->xml_string; |
145
|
0
|
|
|
|
|
0
|
$t = $t2 |
146
|
0
|
|
|
|
|
0
|
} else { push @track_tags, $t->xml_string } # ie don't split the last track portion |
147
|
0
|
|
|
|
|
0
|
} |
148
|
0
|
|
|
|
|
0
|
} |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
my $lap; |
151
|
|
|
|
|
|
|
for my $i (0 .. $#lap_tags) { |
152
|
10
|
|
|
|
|
28
|
my ($lapstring, $last_point_previous_lap); |
153
|
10
|
|
|
|
|
49
|
$lapstring = $lap_tags[$i] . $track_tags[$i]; |
154
|
10
|
|
|
|
|
28
|
$last_point_previous_lap = $lap->trackpoint(-1) if $i > 0; |
155
|
10
|
|
|
|
|
172
|
$lap = Geo::TCX::Lap->new($lapstring, ++$i, $last_point_previous_lap); |
156
|
10
|
50
|
|
|
|
47
|
push @{ $o->{Laps} }, $lap |
157
|
10
|
|
|
|
|
104
|
} |
158
|
10
|
|
|
|
|
27
|
} |
|
10
|
|
|
|
|
218
|
|
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
my $n = $o->laps; |
161
|
|
|
|
|
|
|
die "cannot find any laps, must not be a *.tcx file or str" unless $n; |
162
|
23
|
|
|
|
|
189
|
print "\nFound " . $n, ($n > 1 ? " Laps": " Lap"), "\n\n"; |
163
|
23
|
50
|
|
|
|
84
|
$o->{_txt} = $txt; # only for debugging |
164
|
23
|
100
|
|
|
|
1460
|
$o->set_wd( $opts{work_dir} || $opts{wd} ); |
165
|
23
|
|
|
|
|
182
|
return $o |
166
|
23
|
|
66
|
|
|
273
|
} |
167
|
23
|
|
|
|
|
349
|
|
168
|
|
|
|
|
|
|
=head2 Constructor Methods (object) |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
=over 4 |
171
|
|
|
|
|
|
|
|
172
|
|
|
|
|
|
|
=item activity_to_course( key/values ) |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
returns a new <Geo::TCX> instance as a course, based on the current activity. |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
All I<key/values> are optional: |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
Z<> C<< lap => I<#> >>: converts lap number I<#> to a course, dropping all other laps. All laps are converted if C<lap> is omitted. |
179
|
|
|
|
|
|
|
Z<> C<< course_name => I<$string> >>: the name for the course. The name will be the lap's C<StartTime> if a value is not specified. |
180
|
|
|
|
|
|
|
Z<> C<< filename => I<$filename> >>: will call C<set_filename()> with this value. |
181
|
|
|
|
|
|
|
Z<> C<< work_dir => I<$work_dir> >>: if omitted, it will be set to the same as that of the current object. |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=back |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
=cut |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
my $clone = shift->clone; |
188
|
|
|
|
|
|
|
my %opts = @_; |
189
|
|
|
|
|
|
|
croak 'this instance is already a course' if $clone->is_course; |
190
|
2
|
|
|
2
|
1
|
18
|
my $wd = $opts{work_dir} || $opts{wd} || $clone->set_wd(); |
191
|
2
|
|
|
|
|
16
|
|
192
|
2
|
50
|
|
|
|
13
|
my (@laps, $course); |
193
|
2
|
|
0
|
|
|
11
|
@laps = $opts{lap} ? ($opts{lap}) : (1 .. $clone->laps); |
194
|
|
|
|
|
|
|
|
195
|
2
|
|
|
|
|
6
|
for my $lap_i (@laps) { |
196
|
2
|
100
|
|
|
|
63
|
my $str = $clone->save_laps( [ $lap_i ], nosave => 1, course => 1, course_name => $opts{course_name} ); |
197
|
|
|
|
|
|
|
my $course_i = Geo::TCX->new( \$str, work_dir => $wd ); |
198
|
2
|
|
|
|
|
12
|
if ( defined $course ) { |
199
|
4
|
|
|
|
|
32
|
push @{ $course->{Laps} }, $course_i->lap(1) |
200
|
4
|
|
|
|
|
27
|
} else { $course = $course_i } |
201
|
4
|
100
|
|
|
|
22
|
} |
202
|
2
|
|
|
|
|
7
|
$course->set_filename( $opts{filename} ); |
|
2
|
|
|
|
|
14
|
|
203
|
2
|
|
|
|
|
8
|
return $course |
204
|
|
|
|
|
|
|
} |
205
|
2
|
|
|
|
|
15
|
|
206
|
2
|
|
|
|
|
40
|
=over 4 |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=item clone() |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
Returns a deep copy of a C<Geo::TCX> instance. |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
$clone = $o->clone; |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
=back |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
=cut |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
my $clone; |
219
|
|
|
|
|
|
|
eval(Data::Dumper->Dump([ shift ], ['$clone'])); |
220
|
|
|
|
|
|
|
confess $@ if $@; |
221
|
|
|
|
|
|
|
return $clone |
222
|
6
|
|
|
6
|
1
|
1090
|
} |
223
|
6
|
|
|
|
|
91
|
|
224
|
6
|
50
|
|
|
|
534
|
=head2 Object Methods |
225
|
6
|
|
|
|
|
69
|
|
226
|
|
|
|
|
|
|
=over 4 |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
=item lap( # ) |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
Returns the lap object corresponding to the lap number I<#> specified. I<#> is one-indexed but negative numbers can be used to count from the end, e.g C<-1> to get the last lap. |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
=back |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
=cut |
235
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
my ($o, $lap_i, %exists) = (shift, shift); |
237
|
|
|
|
|
|
|
croak 'requires a single integer as argument' if ! $lap_i or @_; |
238
|
|
|
|
|
|
|
$lap_i = $o->_lap_number($lap_i); |
239
|
|
|
|
|
|
|
return $o->{Laps}[$lap_i-1] |
240
|
|
|
|
|
|
|
} |
241
|
53
|
|
|
53
|
1
|
6843
|
|
242
|
53
|
50
|
33
|
|
|
323
|
=over 4 |
243
|
53
|
|
|
|
|
175
|
|
244
|
53
|
|
|
|
|
375
|
=item laps( qw/ # # ... / ) |
245
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
Returns a list of L<Geo::TCX::Lap> objects corresponding to the lap number(s) specified, or all laps if called without arguments. This method is useful as an access for the number of laps (i.e. without arguments in scalar context). |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
=back |
249
|
|
|
|
|
|
|
|
250
|
|
|
|
|
|
|
=cut |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
my $o = shift; |
253
|
|
|
|
|
|
|
return @{$o->{Laps}} unless @_; |
254
|
|
|
|
|
|
|
my @numbers = @_; |
255
|
|
|
|
|
|
|
my @laps; |
256
|
|
|
|
|
|
|
for my $lap_i (@numbers) { |
257
|
|
|
|
|
|
|
$lap_i = $o->_lap_number($lap_i); |
258
|
97
|
|
|
97
|
1
|
2326
|
push @laps, $o->{Laps}[$lap_i-1] |
259
|
97
|
100
|
|
|
|
246
|
} |
|
95
|
|
|
|
|
291
|
|
260
|
2
|
|
|
|
|
8
|
return @laps |
261
|
2
|
|
|
|
|
5
|
} |
262
|
2
|
|
|
|
|
4
|
|
263
|
4
|
|
|
|
|
9
|
=over 4 |
264
|
4
|
|
|
|
|
12
|
|
265
|
|
|
|
|
|
|
=item merge_laps( #1, #2 ) |
266
|
|
|
|
|
|
|
|
267
|
2
|
|
|
|
|
10
|
Merges lap I<#1> with lap I<#2> and returns true. Both laps must be consecutive laps and the number of laps in the object decreases by one. |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
The C<TotalTimeSeconds> and C<DistanceMeters> aggregates of the merged lap are adjusted. For Activity laps, performance metrics are also adjusted. For Course laps, C<EndPosition> is also adjusted. See L<Geo::TCX::Lap>. |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
=back |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
=cut |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
my ($o, $i1, $i2, %exists) = (shift, shift, shift); |
276
|
|
|
|
|
|
|
croak 'merge_laps() requires two integers as argument' if ! $i2 or @_; |
277
|
|
|
|
|
|
|
croak 'can only merge consecutive laps' unless ($i2 - $i1)==1; |
278
|
|
|
|
|
|
|
my $l1 = $o->lap($i1); |
279
|
|
|
|
|
|
|
my $l2 = $o->lap($i2); |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
my $lap = $l1->merge($l2, as_is => 1); |
282
|
1
|
|
|
1
|
1
|
10
|
|
283
|
1
|
50
|
33
|
|
|
7
|
splice @{ $o->{Laps}}, $i1 - 1, 2, $lap; |
284
|
1
|
50
|
|
|
|
5
|
return 1 |
285
|
1
|
|
|
|
|
3
|
} |
286
|
1
|
|
|
|
|
3
|
|
287
|
|
|
|
|
|
|
=over 4 |
288
|
1
|
|
|
|
|
8
|
|
289
|
|
|
|
|
|
|
=item split_lap( #, $trackpoint_no ) |
290
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
6
|
|
291
|
1
|
|
|
|
|
4
|
Splits lap number I<#> at the specified I<$trackpoint_no> into two laps and returns true. The number of laps in the object increases by one. |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=back |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=cut |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
my ($o, $lap_i, $pt_no, %exists) = (shift, shift, shift); |
298
|
|
|
|
|
|
|
croak 'split_lap() requires two integers as argument' if ! $pt_no or @_; |
299
|
|
|
|
|
|
|
$lap_i = $o->_lap_number($lap_i); |
300
|
|
|
|
|
|
|
my ($lap_a, $lap_b) = $o->lap($lap_i)->split($pt_no); |
301
|
|
|
|
|
|
|
splice @{ $o->{Laps}}, $lap_i -1, 1, ( $lap_a, $lap_b ); |
302
|
|
|
|
|
|
|
return 1 |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
|
305
|
3
|
|
|
3
|
1
|
625
|
=over 4 |
306
|
3
|
50
|
33
|
|
|
27
|
|
307
|
3
|
|
|
|
|
14
|
=item split_lap_at_point_closest_to(#, $point or $trackpoint or $coord_str ) |
308
|
3
|
|
|
|
|
14
|
|
309
|
3
|
|
|
|
|
8
|
Equivalent to C<split_lap()> but splits the specified lap I<#> at the trackpoint that lies closest to a given L<Geo::Gpx::Point>, L<Geo::TCX::Trackpoint>, or a string that can be interpreted as coordinates by C<< Geo::Gpx::Point->flex_coordinates >>. Returns true. |
|
3
|
|
|
|
|
20
|
|
310
|
3
|
|
|
|
|
10
|
|
311
|
|
|
|
|
|
|
=back |
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
=cut |
314
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
my ($o, $lap_i, $to_pt) = (shift, shift, shift); |
316
|
|
|
|
|
|
|
croak 'split_lap_at_point_closest_to() expects two arguments' if @_; |
317
|
|
|
|
|
|
|
$lap_i = $o->_lap_number($lap_i); |
318
|
|
|
|
|
|
|
$to_pt = Geo::Gpx::Point->flex_coordinates( \$to_pt ) unless ref $to_pt; |
319
|
|
|
|
|
|
|
my ($closest_pt, $min_dist, $pt_no) = $o->lap($lap_i)->point_closest_to( $to_pt ); |
320
|
|
|
|
|
|
|
# here we can print some info about the original track and where it will be split |
321
|
|
|
|
|
|
|
$o->split_lap( $lap_i, $pt_no ); |
322
|
|
|
|
|
|
|
return 1 |
323
|
|
|
|
|
|
|
} |
324
|
1
|
|
|
1
|
1
|
4
|
|
325
|
1
|
50
|
|
|
|
5
|
=over 4 |
326
|
1
|
|
|
|
|
5
|
|
327
|
1
|
50
|
|
|
|
18
|
=item time_add( @duration ) |
328
|
1
|
|
|
|
|
127
|
|
329
|
|
|
|
|
|
|
=item time_subtract( @duration ) |
330
|
1
|
|
|
|
|
28
|
|
331
|
1
|
|
|
|
|
14
|
Perform L<DateTime> math on the timestamps of each trackpoint in the track by adding the specified time as per the syntax of L<DateTime>'s C<add()> and C<subtract()> methods. Returns true. |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
Perform L<Date::Time> math on the timestamps of each lap's starttime and trackpoint by adding the specified time as per the syntax of L<Date::Time>'s C<add()> method. Returns true. |
334
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=back |
336
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
=cut |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
my $o = shift; |
340
|
|
|
|
|
|
|
my @duration = @_; |
341
|
|
|
|
|
|
|
my @laps = @{$o->{Laps}}; |
342
|
|
|
|
|
|
|
for my $l (@laps) { |
343
|
|
|
|
|
|
|
$l->time_add( @duration ) |
344
|
|
|
|
|
|
|
} |
345
|
|
|
|
|
|
|
return 1 |
346
|
|
|
|
|
|
|
} |
347
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
my $o = shift; |
349
|
1
|
|
|
1
|
1
|
698
|
my @duration = @_; |
350
|
1
|
|
|
|
|
6
|
my @laps = @{$o->{Laps}}; |
351
|
1
|
|
|
|
|
4
|
for my $l (@laps) { |
|
1
|
|
|
|
|
5
|
|
352
|
1
|
|
|
|
|
3
|
$l->time_subtract( @duration ) |
353
|
4
|
|
|
|
|
23
|
} |
354
|
|
|
|
|
|
|
return 1 |
355
|
1
|
|
|
|
|
9
|
} |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
=over 4 |
358
|
|
|
|
|
|
|
|
359
|
1
|
|
|
1
|
1
|
3
|
=item delete_lap( # ) |
360
|
1
|
|
|
|
|
4
|
|
361
|
1
|
|
|
|
|
3
|
=item keep_lap( # ) |
|
1
|
|
|
|
|
4
|
|
362
|
1
|
|
|
|
|
4
|
|
363
|
4
|
|
|
|
|
24
|
delete or keep the specified lap I<#> form the object. Returns the list of laps removed in both cases. |
364
|
|
|
|
|
|
|
|
365
|
1
|
|
|
|
|
8
|
=back |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
=cut |
368
|
|
|
|
|
|
|
|
369
|
|
|
|
|
|
|
my ($o, $lap_i) = (shift, shift); |
370
|
|
|
|
|
|
|
croak 'requires a single integer as argument' unless $lap_i; |
371
|
|
|
|
|
|
|
$lap_i = $o->_lap_number( $lap_i ); |
372
|
|
|
|
|
|
|
my @removed = splice @{ $o->{Laps}}, $lap_i - 1, 1; |
373
|
|
|
|
|
|
|
return @removed |
374
|
|
|
|
|
|
|
} |
375
|
|
|
|
|
|
|
|
376
|
|
|
|
|
|
|
my ($o, $lap_i) = (shift, shift); |
377
|
|
|
|
|
|
|
my @keep = $o->delete_lap($lap_i); |
378
|
|
|
|
|
|
|
my @removed = @{ $o->{Laps}}; |
379
|
|
|
|
|
|
|
@{ $o->{Laps}} = @keep; |
380
|
|
|
|
|
|
|
return @removed |
381
|
5
|
|
|
5
|
1
|
13
|
} |
382
|
5
|
50
|
|
|
|
14
|
|
383
|
5
|
|
|
|
|
17
|
=over 4 |
384
|
5
|
|
|
|
|
8
|
|
|
5
|
|
|
|
|
16
|
|
385
|
|
|
|
|
|
|
=item save_laps( \@laplist , key/values ) |
386
|
5
|
|
|
|
|
16
|
|
387
|
|
|
|
|
|
|
saves each lap as a separate *.tcx file in the working directory as per <set_wd()>. The filenames will consist of the original source file's name, suffixed by the respective lap number. |
388
|
|
|
|
|
|
|
|
389
|
1
|
|
|
1
|
1
|
5
|
An array reference can be provided to save only a a subset of lap numbers. |
390
|
1
|
|
|
|
|
5
|
|
391
|
1
|
|
|
|
|
3
|
I<key/values> are: |
|
1
|
|
|
|
|
3
|
|
392
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
3
|
|
393
|
|
|
|
|
|
|
Z<> C<course>: converts activity lap(s) as course files if true. |
394
|
1
|
|
|
|
|
4
|
Z<> C<< course_name => $string >>: is only relevant with C<course> and will set the name of the course to I<$string>. |
395
|
|
|
|
|
|
|
Z<> C<force>: overwrites existing files if true, otherwise it won't. |
396
|
|
|
|
|
|
|
Z<> C<indent>: adds white space and indents the xml mark-up in the saved file if true. |
397
|
|
|
|
|
|
|
Z<> C<nosave>: no files are actually saved if true. Useful if only interested in the xml string of the last lap processed. |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
C<course_name> will be ignored if there is more than one lap and the lap's C<StartTime> will be used instead. This is to avoid having multiple files with the same name given that devices use this tag when listing available courses. Acttvity files have an C<Id> tag instead of C<Name> and the laps's C<StartTime> is used at all times. It is easy to edit any of these tags manually in a text editor; just look for the C<< <Name>...</Name> >> tag or C<< <Id>...</Id> >> tags near the top of the files. |
400
|
|
|
|
|
|
|
|
401
|
|
|
|
|
|
|
Returns a string containing the xml of the last lap processed which can subsequently be passed directly to C<< Geo::TCX->new() >> to construct a new instsance. |
402
|
|
|
|
|
|
|
|
403
|
|
|
|
|
|
|
=back |
404
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
=cut |
406
|
|
|
|
|
|
|
|
407
|
|
|
|
|
|
|
my $o = shift; |
408
|
|
|
|
|
|
|
my @laps_to_save; |
409
|
|
|
|
|
|
|
if (ref ($_[0]) eq 'ARRAY') { |
410
|
|
|
|
|
|
|
my $aref = shift; |
411
|
|
|
|
|
|
|
for my $lap_i (@$aref) { |
412
|
|
|
|
|
|
|
push @laps_to_save, $o->lap($lap_i) |
413
|
|
|
|
|
|
|
} |
414
|
|
|
|
|
|
|
} else { @laps_to_save = @{$o->{Laps}} } |
415
|
|
|
|
|
|
|
my %opts = @_; |
416
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
my ($as_course, $fname); |
418
|
|
|
|
|
|
|
$as_course = 1 if $o->is_course or $opts{course}; |
419
|
|
|
|
|
|
|
$fname = $o->set_filename; |
420
|
|
|
|
|
|
|
croak 'no filename found, set_filename(<name>) before saving' unless $fname; |
421
|
9
|
|
|
9
|
1
|
31
|
|
422
|
9
|
|
|
|
|
24
|
# as mentioned in the pod, files will be saved in work_dir as they are new files |
423
|
9
|
100
|
|
|
|
52
|
# use has expectation that that's where working files go |
424
|
5
|
|
|
|
|
11
|
my ($name, $path, $ext) = fileparse( $fname, '\..*' ); |
425
|
5
|
|
|
|
|
27
|
my $wd = $o->set_wd(); |
426
|
5
|
|
|
|
|
27
|
|
427
|
|
|
|
|
|
|
my ($tags_before_lap, $tags_after_lap) = $o->_prep_tags( %opts ); |
428
|
4
|
|
|
|
|
8
|
|
|
4
|
|
|
|
|
16
|
|
429
|
9
|
|
|
|
|
75
|
# Id (for Activity) or Name (for Course) tag |
430
|
|
|
|
|
|
|
my $tag_id_or_name = ''; |
431
|
9
|
|
|
|
|
24
|
if ($as_course) { |
432
|
9
|
100
|
100
|
|
|
45
|
# a bit tricky to determine Name when saving as course, bear with us here |
433
|
9
|
|
|
|
|
35
|
if (@laps_to_save == 1 ) { |
434
|
9
|
50
|
|
|
|
56
|
my $name; |
435
|
|
|
|
|
|
|
if ( defined $opts{course_name} ) { $name = $opts{course_name} } |
436
|
|
|
|
|
|
|
else { |
437
|
|
|
|
|
|
|
if ($o->is_course) { $name = $o->{tag_name} } |
438
|
9
|
|
|
|
|
364
|
else { $name = 'StartTimePlaceHolder' } |
439
|
9
|
|
|
|
|
46
|
} |
440
|
|
|
|
|
|
|
$tag_id_or_name .= '<Name>' . $name . '</Name>' |
441
|
9
|
|
|
|
|
96
|
} else { |
442
|
|
|
|
|
|
|
$tag_id_or_name .= '<Name>' . 'StartTimePlaceHolder' . '</Name>'; |
443
|
|
|
|
|
|
|
# i.e. it's StartTime regardless if more than one lap |
444
|
9
|
|
|
|
|
30
|
} |
445
|
9
|
100
|
|
|
|
39
|
} else { $tag_id_or_name .= '<Id>' . 'StartTimePlaceHolder' . '</Id>' } |
446
|
|
|
|
|
|
|
|
447
|
6
|
50
|
|
|
|
25
|
# Now from what is left below, we can create a save() method to save a file with multilaps. Simply move the $tags_before_lap and $tags_after_lap outside of the loop, continue to add to the $str (i.e. it gets appended to at all times) and we put the saving block outside of the loop at the very end. |
448
|
6
|
|
|
|
|
13
|
# Yah ! And don't distance_net |
449
|
6
|
100
|
|
|
|
22
|
|
|
4
|
|
|
|
|
13
|
|
450
|
|
|
|
|
|
|
my $str; |
451
|
2
|
50
|
|
|
|
9
|
for my $i (0 .. $#laps_to_save) { |
|
2
|
|
|
|
|
6
|
|
452
|
0
|
|
|
|
|
0
|
my $l = $laps_to_save[$i]->clone; |
453
|
|
|
|
|
|
|
$l->distance_net; |
454
|
6
|
|
|
|
|
29
|
|
455
|
|
|
|
|
|
|
$str = $tags_before_lap; |
456
|
0
|
|
|
|
|
0
|
$str .= $tag_id_or_name; |
457
|
|
|
|
|
|
|
$str =~ s/StartTimePlaceHolder/$l->StartTime/e; |
458
|
|
|
|
|
|
|
|
459
|
3
|
|
|
|
|
9
|
my $xml_lap = $l->xml_string( course => $as_course, indent => $opts{indent} ); |
460
|
|
|
|
|
|
|
$str .= $xml_lap; |
461
|
|
|
|
|
|
|
$str .= $tags_after_lap; |
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
unless ($opts{nosave}) { |
464
|
9
|
|
|
|
|
29
|
my $fname_lap = $wd . $name . '-Lap-' . ($i+1) . $ext; |
465
|
9
|
|
|
|
|
36
|
croak "$fname_lap already exists" if -f $fname_lap and !$opts{force}; |
466
|
15
|
|
|
|
|
168
|
open(my $fh, '>', $fname_lap) or die "can't open $fname_lap $!"; |
467
|
15
|
|
|
|
|
138
|
print $fh $str |
468
|
|
|
|
|
|
|
} |
469
|
15
|
|
|
|
|
45
|
} |
470
|
15
|
|
|
|
|
48
|
return $str |
471
|
15
|
|
|
|
|
357
|
} |
|
9
|
|
|
|
|
97
|
|
472
|
|
|
|
|
|
|
|
473
|
15
|
|
|
|
|
121
|
=over 4 |
474
|
15
|
|
|
|
|
144
|
|
475
|
15
|
|
|
|
|
49
|
=item save( key/values ) |
476
|
|
|
|
|
|
|
|
477
|
15
|
100
|
|
|
|
108
|
saves the current instance. |
478
|
10
|
|
|
|
|
51
|
|
479
|
10
|
50
|
66
|
|
|
514
|
I<key/values> are: |
480
|
10
|
50
|
|
|
|
1376
|
|
481
|
10
|
|
|
|
|
1743
|
Z<> C<filename>: the name of the file to be saved. Has the effect calling C<set_filename()> and changes the name of the file in the current instance (e.g. akin to "save as" in many applications). |
482
|
|
|
|
|
|
|
Z<> C<force>: overwrites existing files if true, otherwise it won't. |
483
|
|
|
|
|
|
|
Z<> C<indent>: adds white space and indents the xml mark-up in the saved file if true. |
484
|
9
|
|
|
|
|
193
|
|
485
|
|
|
|
|
|
|
Returns a string containing the xml representation of the file. |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
=back |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
=cut |
490
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
my ($o, %opts) = @_; |
492
|
|
|
|
|
|
|
|
493
|
|
|
|
|
|
|
my $fname; |
494
|
|
|
|
|
|
|
if ( $opts{filename} ) { $fname = $o->set_filename( $opts{filename} ) } |
495
|
|
|
|
|
|
|
else { $fname = $o->set_filename() } |
496
|
|
|
|
|
|
|
croak 'no filename found, provide one with set_filename() or use key \'filename\'' unless $fname; |
497
|
|
|
|
|
|
|
croak "$fname already exists" if -f $fname and !$opts{force}; |
498
|
|
|
|
|
|
|
|
499
|
|
|
|
|
|
|
my ($tags_before_lap, $tags_after_lap) = $o->_prep_tags( indent => $opts{indent} ); |
500
|
|
|
|
|
|
|
my $str = $tags_before_lap; |
501
|
|
|
|
|
|
|
if ($o->is_course) { $str .= '<Name>' . $o->{tag_name} . '</Name>' } |
502
|
|
|
|
|
|
|
else { $str .= '<Id>' . $o->{tag_id} . '</Id>' } |
503
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
my ($str_activity_laps, $str_course_laps, $str_course_tracks); |
505
|
|
|
|
|
|
|
for my $lap ($o->laps) { |
506
|
2
|
|
|
2
|
1
|
659
|
my $str_lap = $lap->xml_string( indent => $opts{indent} ); |
507
|
|
|
|
|
|
|
|
508
|
2
|
|
|
|
|
12
|
if ($lap->is_course) { |
509
|
2
|
100
|
|
|
|
14
|
# for courses, the xml track tags are not nested within the lap |
|
1
|
|
|
|
|
5
|
|
510
|
1
|
|
|
|
|
3
|
# tags but follow them instead. Yah, weird. So need to collect |
511
|
2
|
50
|
|
|
|
20
|
# the strings seperately then assemble after the loop |
512
|
2
|
50
|
33
|
|
|
80
|
if ( $str_lap =~ s,\s*(<Lap>.*</Lap>)\s*(<Track>.*</Track>)\s*,,s ) { |
513
|
|
|
|
|
|
|
$str_course_laps .= $1; |
514
|
2
|
|
|
|
|
29
|
$str_course_tracks .= $2 |
515
|
2
|
|
|
|
|
12
|
} else { croak "cannot find lap or track tags in Laps object" } |
516
|
2
|
50
|
|
|
|
21
|
} else { |
|
2
|
|
|
|
|
16
|
|
517
|
0
|
|
|
|
|
0
|
$str_activity_laps .= $str_lap |
518
|
|
|
|
|
|
|
} |
519
|
2
|
|
|
|
|
14
|
} |
520
|
2
|
|
|
|
|
22
|
|
521
|
4
|
|
|
|
|
45
|
# Flatten the course tracks into a a single track |
522
|
|
|
|
|
|
|
$str_course_tracks =~ s,</Track>\s*<Track>,,gs if $str_course_tracks; |
523
|
4
|
50
|
|
|
|
30
|
|
524
|
|
|
|
|
|
|
if ($o->is_course) { $str .= $str_course_laps . $str_course_tracks } |
525
|
|
|
|
|
|
|
else { $str .= $str_activity_laps } |
526
|
|
|
|
|
|
|
|
527
|
4
|
50
|
|
|
|
199
|
$str .= $tags_after_lap; |
528
|
4
|
|
|
|
|
36
|
|
529
|
4
|
|
|
|
|
39
|
open(my $fh, '>', $fname) or die "can't open $fname $!"; |
530
|
0
|
|
|
|
|
0
|
print $fh $str; |
531
|
|
|
|
|
|
|
return $str |
532
|
0
|
|
|
|
|
0
|
} |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
my ($o, %opts) = @_; |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
# identical to save_laps() |
537
|
2
|
50
|
|
|
|
61
|
my ($newline, $tab, $as_course); |
538
|
|
|
|
|
|
|
$newline = $opts{indent} ? "\n" : ''; |
539
|
2
|
50
|
|
|
|
12
|
$tab = $opts{indent} ? ' ' : ''; |
|
2
|
|
|
|
|
42
|
|
540
|
0
|
|
|
|
|
0
|
$as_course = 1 if $o->is_course or $opts{course}; |
541
|
|
|
|
|
|
|
|
542
|
2
|
|
|
|
|
29
|
# |
543
|
|
|
|
|
|
|
# Prepare the mark-up that appears *outside* the laps (therefore will be common to all saved laps) |
544
|
2
|
50
|
|
|
|
358
|
|
545
|
2
|
|
|
|
|
166
|
# These tag collection blocks could be shortened but it might not become more legible. |
546
|
2
|
|
|
|
|
90
|
# The many variables help for debugging as the resulting string can be an extremely |
547
|
|
|
|
|
|
|
# long flat string |
548
|
|
|
|
|
|
|
|
549
|
|
|
|
|
|
|
# we first collect the tags we need, we assemble them later |
550
|
11
|
|
|
11
|
|
62
|
# we need these 3 pairs of tags so declare in a block |
551
|
|
|
|
|
|
|
my ($tag_open_trainctrdb, $tag_close_trainctrdb); |
552
|
|
|
|
|
|
|
my ($tag_open_activity_or_course_plural, $tag_close_activity_or_course_plural ); |
553
|
11
|
|
|
|
|
26
|
my ($tag_open_activity_or_course_singular, $tag_close_activity_or_course_singular); |
554
|
11
|
100
|
|
|
|
63
|
|
555
|
11
|
100
|
|
|
|
61
|
if ($o->{tag_trainingcenterdatabase} =~ /(<TrainingCenterDatabase[^>]*>)/) { |
556
|
11
|
100
|
100
|
|
|
46
|
$tag_open_trainctrdb = $1; |
557
|
|
|
|
|
|
|
$tag_close_trainctrdb = '</TrainingCenterDatabase>' |
558
|
|
|
|
|
|
|
} else { croak 'can\'t find the expected <TrainingCenterDatabase ...> tag' } |
559
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
# in history files (in mine at least), these tags ever appear only once, nesting all of the data within them |
561
|
|
|
|
|
|
|
# <Activities><Activity Sport="Biking"> |
562
|
|
|
|
|
|
|
# <Courses><Course> |
563
|
|
|
|
|
|
|
# Activity is nested within Activities and similarly Course is nested within Courses |
564
|
|
|
|
|
|
|
|
565
|
|
|
|
|
|
|
if ($o->{tag_activities}) { |
566
|
|
|
|
|
|
|
if ( $o->{tag_activities} =~ /(<Activities[^>]*>)/) { |
567
|
11
|
|
|
|
|
72
|
$tag_open_activity_or_course_plural = $1 |
568
|
11
|
|
|
|
|
0
|
} else { croak 'can\'t find the expected <Activities> tag' } |
569
|
11
|
|
|
|
|
0
|
if ( $o->{tag_activity} =~ /(<Activity[^>]*>)/ ) { |
570
|
|
|
|
|
|
|
$tag_open_activity_or_course_singular = $1 |
571
|
11
|
50
|
|
|
|
142
|
} else { croak 'can\'t find the expected <Activity Sport="..."> tag' } |
572
|
11
|
|
|
|
|
58
|
($tag_close_activity_or_course_singular, $tag_close_activity_or_course_plural) = ('</Activity>', '</Activities>') |
573
|
11
|
|
|
|
|
26
|
} |
574
|
0
|
|
|
|
|
0
|
if ($o->{tag_courses}) { |
575
|
|
|
|
|
|
|
if ( $o->{tag_courses} =~ /(<Courses[^>]*>)/) { |
576
|
|
|
|
|
|
|
$tag_open_activity_or_course_plural = $1 |
577
|
|
|
|
|
|
|
} else { croak 'can\'t find the expected <Courses> tag' } |
578
|
|
|
|
|
|
|
if ( $o->{tag_course} =~ /(<Course(?!s)[^>]*>)/ ) { |
579
|
|
|
|
|
|
|
$tag_open_activity_or_course_singular = $1 |
580
|
|
|
|
|
|
|
} else { croak 'can\'t find the expected <Course> tag' } |
581
|
11
|
100
|
|
|
|
40
|
($tag_close_activity_or_course_singular, $tag_close_activity_or_course_plural) = ('</Course>', '</Courses>') |
582
|
7
|
50
|
|
|
|
51
|
} |
583
|
7
|
|
|
|
|
21
|
if ($as_course and !$o->{tag_courses}) { # i.e. when saving an activity as a course |
584
|
0
|
|
|
|
|
0
|
$tag_open_activity_or_course_plural = '<Courses>'; |
585
|
7
|
50
|
|
|
|
44
|
$tag_open_activity_or_course_singular = '<Course>'; |
586
|
7
|
|
|
|
|
21
|
($tag_close_activity_or_course_singular, $tag_close_activity_or_course_plural) = ('</Course>', '</Courses>') |
587
|
0
|
|
|
|
|
0
|
} |
588
|
7
|
|
|
|
|
24
|
|
589
|
|
|
|
|
|
|
# assembling the tags to get the mark-up that appears *before* the laps |
590
|
11
|
100
|
|
|
|
47
|
my $tags_before_lap = ''; |
591
|
4
|
50
|
|
|
|
49
|
$tags_before_lap = $o->{tag_xml_version} . "\n"; |
592
|
4
|
|
|
|
|
20
|
$tags_before_lap .= $tag_open_trainctrdb; |
593
|
0
|
|
|
|
|
0
|
$tags_before_lap .= $newline . $tab . $tag_open_activity_or_course_plural; |
594
|
4
|
50
|
|
|
|
42
|
$tags_before_lap .= $newline . ($tab x 2) . $tag_open_activity_or_course_singular; |
595
|
4
|
|
|
|
|
18
|
$tags_before_lap .= $newline . ($tab x 3); |
596
|
0
|
|
|
|
|
0
|
|
597
|
4
|
|
|
|
|
23
|
# assembling the tags to get the mark-up that appears *after* the laps |
598
|
|
|
|
|
|
|
my ($tags_after_lap) = ''; |
599
|
11
|
100
|
100
|
|
|
90
|
$tags_after_lap = $newline . ($tab x 3) . $o->{tag_creator} if $o->{tag_creator}; |
600
|
4
|
|
|
|
|
12
|
$tags_after_lap .= $newline . ($tab x 2) . $tag_close_activity_or_course_singular; |
601
|
4
|
|
|
|
|
9
|
$tags_after_lap .= $newline . $tab . $tag_close_activity_or_course_plural; |
602
|
4
|
|
|
|
|
12
|
$tags_after_lap .= $newline . $tab . $o->{tag_author} if $o->{tag_author}; |
603
|
|
|
|
|
|
|
$tags_after_lap .= $newline . $tag_close_trainctrdb; |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
return $tags_before_lap, $tags_after_lap |
606
|
11
|
|
|
|
|
34
|
} |
607
|
11
|
|
|
|
|
37
|
|
608
|
11
|
|
|
|
|
34
|
=over 4 |
609
|
11
|
|
|
|
|
37
|
|
610
|
11
|
|
|
|
|
47
|
=item set_filename( $filename ) |
611
|
11
|
|
|
|
|
40
|
|
612
|
|
|
|
|
|
|
Sets/gets the filename. Returns the name of the file with the complete path. |
613
|
|
|
|
|
|
|
|
614
|
11
|
|
|
|
|
32
|
=back |
615
|
11
|
100
|
|
|
|
59
|
|
616
|
11
|
|
|
|
|
35
|
=cut |
617
|
11
|
|
|
|
|
29
|
|
618
|
11
|
100
|
|
|
|
52
|
my ($o, $fname) = (shift, shift); |
619
|
11
|
|
|
|
|
33
|
return $o->{_fileABSOLUTENAME} unless $fname; |
620
|
|
|
|
|
|
|
croak 'set_filename() takes only a single name as argument' if @_; |
621
|
11
|
|
|
|
|
62
|
my $wd; |
622
|
|
|
|
|
|
|
if ($o->_is_wd_defined) { $wd = $o->set_wd } |
623
|
|
|
|
|
|
|
# set_filename gets called before set_wd by new() so can't access work_dir until initialized |
624
|
|
|
|
|
|
|
|
625
|
|
|
|
|
|
|
my ($name, $path, $ext); |
626
|
|
|
|
|
|
|
($name, $path, $ext) = fileparse( $fname, '\..*' ); |
627
|
|
|
|
|
|
|
if ($wd) { |
628
|
|
|
|
|
|
|
if ( ! ($fname =~ /^\// ) ) { |
629
|
|
|
|
|
|
|
# ie if fname is not an abolsute path, adjust $path to be relative to work_dir |
630
|
|
|
|
|
|
|
($name, $path, $ext) = fileparse( $wd . $fname, '\..*' ) |
631
|
|
|
|
|
|
|
} |
632
|
|
|
|
|
|
|
} |
633
|
|
|
|
|
|
|
$o->{_fileABSOLUTEPATH} = abs_path( $path ) . '/'; |
634
|
|
|
|
|
|
|
$o->{_fileABSOLUTENAME} = $o->{_fileABSOLUTEPATH} . $name . $ext; |
635
|
60
|
|
|
60
|
1
|
2028
|
croak 'directory ' . $o->{_fileABSOLUTEPATH} . ' doesn\'t exist' unless -d $o->{_fileABSOLUTEPATH}; |
636
|
60
|
100
|
|
|
|
480
|
$o->{_fileNAME} = $name; |
637
|
28
|
50
|
|
|
|
118
|
$o->{_filePATH} = $path; |
638
|
28
|
|
|
|
|
60
|
$o->{_fileEXT} = $ext; |
639
|
28
|
100
|
|
|
|
110
|
$o->{_filePARSEDNAME} = $fname; |
|
10
|
|
|
|
|
60
|
|
640
|
|
|
|
|
|
|
# _file* keys only for debugging, should not be used anywhere else |
641
|
|
|
|
|
|
|
return $o->{_fileABSOLUTENAME} |
642
|
28
|
|
|
|
|
81
|
} |
643
|
28
|
|
|
|
|
1430
|
|
644
|
28
|
100
|
|
|
|
147
|
=over 4 |
645
|
10
|
100
|
|
|
|
42
|
|
646
|
|
|
|
|
|
|
=item set_wd( $folder ) |
647
|
8
|
|
|
|
|
164
|
|
648
|
|
|
|
|
|
|
Sets/gets the working directory for any eventual saving of the *.tcx file and checks the validity of that path. It can be set as a relative path (i.e. relative to the actual L<Cwd>) or as an absolute path, but is always returned as a full path. |
649
|
|
|
|
|
|
|
|
650
|
28
|
|
|
|
|
964
|
This working directory is always defined. The previous one is also stored in memory, such that C<set_wd('-')> switches back and forth between two directories. The module never actually C<chdir>'s, it just keeps track of where the user wishes to save files. |
651
|
28
|
|
|
|
|
173
|
|
652
|
28
|
50
|
|
|
|
392
|
=back |
653
|
28
|
|
|
|
|
296
|
|
654
|
28
|
|
|
|
|
127
|
=cut |
655
|
28
|
|
|
|
|
78
|
|
656
|
28
|
|
|
|
|
70
|
my ($o, $dir) = (shift, shift); |
657
|
|
|
|
|
|
|
croak 'set_wd() takes only a single folder as argument' if @_; |
658
|
|
|
|
|
|
|
my $first_call = ! $o->_is_wd_defined; # ie if called for 1st time -- at construction by new() |
659
|
28
|
|
|
|
|
138
|
|
660
|
|
|
|
|
|
|
if (! $dir) { |
661
|
|
|
|
|
|
|
return $o->{work_dir} unless $first_call; |
662
|
|
|
|
|
|
|
my $fname = $o->set_filename; |
663
|
|
|
|
|
|
|
if ($fname) { |
664
|
|
|
|
|
|
|
my ($name, $path, $ext) = fileparse( $fname ); |
665
|
|
|
|
|
|
|
$o->set_wd( $path ) |
666
|
|
|
|
|
|
|
} else { $o->set_wd( cwd ) } |
667
|
|
|
|
|
|
|
} else { |
668
|
|
|
|
|
|
|
$dir =~ s/^\s+|\s+$//g; # some clean-up |
669
|
|
|
|
|
|
|
$dir =~ s/~/$ENV{'HOME'}/ if $dir =~ /^~/; |
670
|
|
|
|
|
|
|
$dir = $o->_set_wd_old if $dir eq '-'; |
671
|
|
|
|
|
|
|
|
672
|
|
|
|
|
|
|
if ($dir =~ m,^[^/], ) { # convert rel path to full |
673
|
|
|
|
|
|
|
$dir = $first_call ? cwd . '/' . $dir : $o->{work_dir} . $dir |
674
|
83
|
|
|
83
|
1
|
973
|
} |
675
|
83
|
50
|
|
|
|
265
|
$dir =~ s,/*$,/,; # some more cleaning |
676
|
83
|
|
|
|
|
252
|
1 while ( $dir =~ s,/\./,/, ); # support '.' |
677
|
|
|
|
|
|
|
1 while ( $dir =~ s,[^/]+/\.\./,, ); # and '..' |
678
|
83
|
100
|
|
|
|
244
|
croak "$dir not a valid directory" unless -d $dir; |
679
|
44
|
100
|
|
|
|
289
|
|
680
|
16
|
|
|
|
|
67
|
if ($first_call) { $o->_set_wd_old( $dir ) } |
681
|
16
|
100
|
|
|
|
50
|
else { $o->_set_wd_old( $o->{work_dir} ) } |
682
|
15
|
|
|
|
|
471
|
$o->{work_dir} = $dir |
683
|
15
|
|
|
|
|
85
|
} |
684
|
1
|
|
|
|
|
8893
|
return $o->{work_dir} |
685
|
|
|
|
|
|
|
} |
686
|
39
|
|
|
|
|
308
|
|
687
|
39
|
50
|
|
|
|
146
|
# if ($o->set_filename) { $o->set_wd() } # if we have a filename |
688
|
39
|
100
|
|
|
|
122
|
# else { $o->set_wd( cwd ) } # if we don't |
689
|
|
|
|
|
|
|
|
690
|
39
|
100
|
|
|
|
172
|
my ($o, $dir) = @_; |
691
|
8
|
100
|
|
|
|
13527
|
$o->{work_dir_old} = $dir if $dir; |
692
|
|
|
|
|
|
|
return $o->{work_dir_old} |
693
|
39
|
|
|
|
|
466
|
} |
694
|
39
|
|
|
|
|
180
|
|
695
|
39
|
|
|
|
|
210
|
|
696
|
39
|
50
|
|
|
|
1231
|
=over 4 |
697
|
|
|
|
|
|
|
|
698
|
39
|
100
|
|
|
|
168
|
=item is_activity() |
|
23
|
|
|
|
|
139
|
|
699
|
16
|
|
|
|
|
60
|
|
700
|
39
|
|
|
|
|
117
|
=item is_course() |
701
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
True if the C<Geo::TCX> instance is a of the type indicated by the method, false otherwise. |
703
|
55
|
|
|
|
|
215
|
|
704
|
|
|
|
|
|
|
=back |
705
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
=cut |
707
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
|
709
|
44
|
|
|
44
|
|
163
|
=over 4 |
710
|
44
|
100
|
|
|
|
184
|
|
711
|
|
|
|
|
|
|
=item activity( $string ) |
712
|
44
|
|
|
|
|
104
|
|
713
|
|
|
|
|
|
|
Gets/sets the Activity type as detected from C<\<Activity Sport="*"\>>, sets it to I<$string> if provided. Garmin devices (at least the Edge) record activities as being of types 'Running', 'Biking', 'MultiSport', etc. |
714
|
111
|
|
|
111
|
|
454
|
|
715
|
|
|
|
|
|
|
=back |
716
|
|
|
|
|
|
|
|
717
|
|
|
|
|
|
|
=cut |
718
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
my ($o, $activity) = @_; |
720
|
|
|
|
|
|
|
# should I check what activity types are allowed? Must they be single words? |
721
|
|
|
|
|
|
|
if ($activity) { |
722
|
|
|
|
|
|
|
$o->{tag_activity} =~ s,(\<Activity Sport=)"[^"]*",$1"$activity",; |
723
|
|
|
|
|
|
|
return $o->activity |
724
|
|
|
|
|
|
|
} |
725
|
|
|
|
|
|
|
$activity = $1 if $o->{tag_activity} =~ /<Activity Sport="([^"]*)"/; |
726
|
|
|
|
|
|
|
return $activity |
727
|
|
|
|
|
|
|
} |
728
|
2
|
|
|
2
|
1
|
15
|
|
729
|
30
|
|
|
30
|
1
|
252
|
=over 4 |
730
|
|
|
|
|
|
|
|
731
|
|
|
|
|
|
|
=item author( key/value ) |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
Gets/sets the fields of the Author tag. Supported keys are C<Name>, C<LangID>, C<PartNumber> and all excpect a string as value. |
734
|
|
|
|
|
|
|
|
735
|
|
|
|
|
|
|
The C<Build> field can also be accesses but the intent is to set it, the string supplied should be in the form of an xml string in the way this tag appears in a *.tcx file (e.g. Version, VersionMajor, VersionMinor, Type, …). Simply access that key of the returned hash ref to see what is should look like. |
736
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
Returns a hash reference of key/value pairs. |
738
|
|
|
|
|
|
|
|
739
|
|
|
|
|
|
|
This method is under development and behaviour could change in the future. |
740
|
|
|
|
|
|
|
|
741
|
|
|
|
|
|
|
=back |
742
|
6
|
|
|
6
|
1
|
13
|
|
743
|
|
|
|
|
|
|
=cut |
744
|
6
|
100
|
|
|
|
15
|
|
745
|
2
|
|
|
|
|
93
|
# the only purpose I have for this at this stage is to set the name mostly, we'll see if I have a need for other, but will take the Build key as is and not set up sub-keys yet. |
746
|
2
|
|
|
|
|
11
|
# I mostly want to use this method so that I can set it if I want for any *.tcx I generate (Courses, save laps), with the version number of my module as well |
747
|
|
|
|
|
|
|
# the Build entries contain integers but I am not supporting this at this point |
748
|
4
|
50
|
|
|
|
25
|
# |
749
|
4
|
|
|
|
|
13
|
# <Author xsi:type="Application_t" |
750
|
|
|
|
|
|
|
# <Name>string</Name> |
751
|
|
|
|
|
|
|
# <Build containis |
752
|
|
|
|
|
|
|
# <Version> |
753
|
|
|
|
|
|
|
# VersionMajor |
754
|
|
|
|
|
|
|
# VersionMinor |
755
|
|
|
|
|
|
|
# BuildMajor |
756
|
|
|
|
|
|
|
# BuildMinor |
757
|
|
|
|
|
|
|
# </Version> |
758
|
|
|
|
|
|
|
# <Type>Release<Type> |
759
|
|
|
|
|
|
|
# </Build> |
760
|
|
|
|
|
|
|
# <LangID>EN</LangID> |
761
|
|
|
|
|
|
|
# <PartNumber>digit and dash string</PartNumber> |
762
|
|
|
|
|
|
|
# </Author> |
763
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
my %possible_author_keys; |
765
|
|
|
|
|
|
|
my @author_keys = qw/ Name Build LangID PartNumber /; |
766
|
|
|
|
|
|
|
$possible_author_keys{$_} = 1 for @author_keys; |
767
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
# NB: similar to the _file* keys, the _Author href can not exist at any time |
769
|
|
|
|
|
|
|
my $o = shift; |
770
|
|
|
|
|
|
|
$o->{_Author} = {}; |
771
|
|
|
|
|
|
|
my $href = $o->{_Author}; |
772
|
|
|
|
|
|
|
my $author_xml; |
773
|
|
|
|
|
|
|
if ( $o->{tag_author} =~ m,<Author\s+([^=]+="[^"]+")>(.*)<\/Author>, ) { |
774
|
|
|
|
|
|
|
$href->{string_inside_author_tag} = $1; |
775
|
|
|
|
|
|
|
$author_xml = $2 |
776
|
|
|
|
|
|
|
} |
777
|
|
|
|
|
|
|
for my $key (@author_keys) { |
778
|
|
|
|
|
|
|
$href->{$key} = $1 if $author_xml =~ m,<$key>(.+)</$key>, |
779
|
|
|
|
|
|
|
} |
780
|
|
|
|
|
|
|
return $o->{tag_author} |
781
|
|
|
|
|
|
|
} |
782
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
my $o = shift; |
784
|
|
|
|
|
|
|
my $href = $o->{_Author}; |
785
|
|
|
|
|
|
|
|
786
|
|
|
|
|
|
|
my $str = '<Author ' . $href->{string_inside_author_tag} . '>'; |
787
|
|
|
|
|
|
|
for my $key (@author_keys) { |
788
|
|
|
|
|
|
|
$str .= "<$key>" . $href->{$key} . "</$key>" if defined $href->{key} |
789
|
|
|
|
|
|
|
} |
790
|
|
|
|
|
|
|
$str .= '</Author>'; |
791
|
|
|
|
|
|
|
$o->{tag_author} = $str; |
792
|
|
|
|
|
|
|
return $o->{tag_author} |
793
|
17
|
|
|
17
|
|
47
|
} |
794
|
17
|
|
|
|
|
69
|
|
795
|
17
|
|
|
|
|
62
|
my ($o, %keys_values) = @_; |
796
|
17
|
|
|
|
|
45
|
my $href = $o->{_Author}; |
797
|
17
|
50
|
|
|
|
156
|
croak 'no author tag found in object' if (!%keys_values and ! $href); |
798
|
17
|
|
|
|
|
84
|
if (%keys_values) { |
799
|
17
|
|
|
|
|
88
|
for my $key (keys %keys_values) { |
800
|
|
|
|
|
|
|
croak 'unsupported Author field' unless $possible_author_keys{$key}; |
801
|
17
|
|
|
|
|
83
|
$href->{$key} = $keys_values{$key} |
802
|
68
|
100
|
|
|
|
1585
|
} |
803
|
|
|
|
|
|
|
$o->_update_author_tag |
804
|
|
|
|
|
|
|
} |
805
|
17
|
|
|
|
|
156
|
return $href |
806
|
|
|
|
|
|
|
} |
807
|
|
|
|
|
|
|
|
808
|
2
|
|
|
2
|
|
5
|
# returns the actual lap number if a negative index is passed to count from the end |
809
|
2
|
|
|
|
|
4
|
my ($o, $lap_i, $n, %exists) = (shift, shift); |
810
|
|
|
|
|
|
|
$n = $o->laps; |
811
|
2
|
|
|
|
|
8
|
$lap_i += $n + 1 if $lap_i < 0; |
812
|
2
|
|
|
|
|
5
|
$exists{$_} = 1 for (1 .. $n); |
813
|
|
|
|
|
|
|
croak "Lap $lap_i does not exist" unless $exists{$lap_i}; |
814
|
8
|
50
|
|
|
|
17
|
return $lap_i |
815
|
2
|
|
|
|
|
6
|
} |
816
|
2
|
|
|
|
|
5
|
|
817
|
|
|
|
|
|
|
=head1 EXAMPLES |
818
|
2
|
|
|
|
|
4
|
|
819
|
|
|
|
|
|
|
Coming soon. |
820
|
|
|
|
|
|
|
|
821
|
3
|
|
|
3
|
1
|
1325
|
=head1 BUGS |
822
|
3
|
|
|
|
|
8
|
|
823
|
3
|
50
|
66
|
|
|
16
|
Nothing to report yet. |
824
|
3
|
100
|
|
|
|
9
|
|
825
|
2
|
|
|
|
|
8
|
=head1 AUTHOR |
826
|
2
|
50
|
|
|
|
8
|
|
827
|
2
|
|
|
|
|
6
|
Patrick Joly |
828
|
|
|
|
|
|
|
|
829
|
|
|
|
|
|
|
=head1 VERSION |
830
|
2
|
|
|
|
|
7
|
|
831
|
3
|
|
|
|
|
8
|
1.02 |
832
|
|
|
|
|
|
|
|
833
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
834
|
|
|
|
|
|
|
|
835
|
|
|
|
|
|
|
Copyright (c) 2022, Patrick Joly C<< <patjol@cpan.org> >>. All rights reserved. |
836
|
66
|
|
|
66
|
|
154
|
|
837
|
66
|
|
|
|
|
147
|
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L<perlartistic>. |
838
|
66
|
100
|
|
|
|
175
|
|
839
|
66
|
|
|
|
|
385
|
=head1 SEE ALSO |
840
|
66
|
50
|
|
|
|
181
|
|
841
|
66
|
|
|
|
|
176
|
L<Geo::Gpx> |
842
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
=head1 DISCLAIMER OF WARRANTY |
844
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. |
846
|
|
|
|
|
|
|
|
847
|
|
|
|
|
|
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENSE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. |
848
|
|
|
|
|
|
|
|
849
|
|
|
|
|
|
|
=cut |
850
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
1; |
852
|
|
|
|
|
|
|
|