File Coverage

script/fit2tcx.pl
Criterion Covered Total %
statement 280 466 60.0
branch 89 264 33.7
condition 33 126 26.1
subroutine 19 24 79.1
pod n/a
total 421 880 47.8


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2 6     6   32069 use strict;
  6         10  
  6         272  
3 6     6   27 use warnings;
  6         10  
  6         534  
4              
5 6         1302606 our $VERSION = '1.13';
6              
7             =encoding utf-8
8              
9             =head1 NAME
10              
11             fit2tcx.pl - script to convert a FIT file to a TCX file
12              
13             =head1 SYNOPSIS
14              
15             fit2tcx.pl [ --must=$list --tp_exclude=$list --indent=# --verbose ] $fit_activity_file [ $new_filename ]
16             fit2tcx.pl --help
17             fit2tcx.pl --version
18              
19             =head1 DESCRIPTION
20              
21             C reads the contents of a I<$fit_activity_file> and converts it to correspoding TCX format. If <$new_filename> is provided, writes the resulting TCX content to it, otherwise prints the content to standard output.
22              
23             Will raise an exception if the I<$fit_activity_file> already has a C<.tcx> extension.
24              
25             =cut
26              
27 6     6   3225 use FindBin;
  6         9236  
  6         393  
28 6     6   4600 use lib $FindBin::Bin;
  6         4967  
  6         38  
29              
30 6     6   11883 use Geo::FIT;
  6         55  
  6         3852  
31 6     6   72 use POSIX qw(strftime);
  6         14  
  6         58  
32 6     6   564 use IO::Handle;
  6         9  
  6         211  
33 6     6   53 use FileHandle;
  6         11  
  6         46  
34 6     6   8961 use Getopt::Long;
  6         106318  
  6         33  
35 6     6   1578 use Time::Local;
  6         15  
  6         58204  
36              
37 6         37 my ($must, $tp_exclude, $indent_n, $verbose, $version, $help) = ('Time', '', 2, 0);
38 0     0   0 sub usage { "Usage: $0 [ --help --must=\$list --tp_exclude=\$list --indent=# --verbose ] \$fit_activity_file [ \$new_filename ]\n" }
39              
40 6 50       63 GetOptions( "must=s" => \$must,
41             "tp_exclude=s" => \$tp_exclude,
42             "indent=i" => \$indent_n,
43             "verbose" => \$verbose,
44             "version" => \$version,
45             "help" => \$help,
46             ) or die usage();
47              
48 6 100       7577 if ($version) {
49 3         24 print $0, " version: ", $VERSION, "\n";
50             exit
51 3         0 }
52 3 50       14 die usage() if $help;
53              
54 3         12 my ($from, $to) = qw(- -);
55 3 50       13 if (@ARGV) {
56 3         7 $from = shift @ARGV;
57 3 50       13 @ARGV and $to = shift @ARGV
58             }
59 3 100       26 if ($from =~ /\.tcx\s*$/i) {
60 1         0 die "skipped converting $from: already has *.tcx extension and likely not a FIT file"
61             }
62              
63             # consider adding $double_precision to GetOptions(), renaming $pf to $print_format
64 2         5 my $double_precision = 7;
65 2 50       15 my $pf = $double_precision eq '' ? 'g' : '.' . $double_precision . 'f';
66              
67 2         4 my (@must, @tp_exclude);
68 2         10 @must = split /,/, $must;
69 2         8 @tp_exclude = split /,/, $tp_exclude;
70              
71             =head2 Options
72              
73             =over 4
74              
75             =item C<< --must=I >>
76              
77             specifies a comma separated list of TCX elements which must be included in trackpoints.
78              
79             C convert each C message to a trackpoint in TCX format, examines whether or not any of the elements in the list are defined, and drop the trackpoint if not.
80              
81             Some map services seem to require a TCX file created with C<-must=Time,Position> option.
82              
83             =item C<< --tp_exclude=I >>
84              
85             specifies a comma separated list of TCX elements which should be excluded from C elements in I.
86              
87             For instance, with C<< --tp_exclude=AltitudeMeters >>, the resulting TCX file will contain no altitude data in it's Cs.
88              
89             =item C<< --indent=I<#> >>
90              
91             specifies the number of spaces to use in indenting the XML tags of the TCX file.
92              
93             =item C<< --verbose >>
94              
95             shows FIT file header and trailing CRC information on standard output.
96              
97             =back
98              
99             =cut
100              
101             # Parameters (were options in the initial version)
102 2         4 my $pw_fix = 1;
103 2         5 my $pw_fix_b = 0;
104 2         4 my $tplimit = 0;
105 2         4 my $tplimit_smart = 1;
106 2         4 my $include_creator = 1;
107 2         6 my $lap = '';
108 2         4 my $lap_start = 0;
109 2         5 my $lap_max = ~(~0 << 16) - 1;
110 2         4 my $tpmask = '';
111 2         5 my $tpfake = '';
112              
113             # Parameters
114 2         6 my $tcdns = 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2';
115 2         4 my $tcdxsd = 'http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd';
116 2         8 my $fcns = 'http://www.garmin.com/xmlschemas/FatCalories/v1';
117 2         5 my $fcxsd = 'http://www.garmin.com/xmlschemas/fatcalorieextensionv1.xsd';
118 2         5 my $tpxns = 'http://www.garmin.com/xmlschemas/ActivityExtension/v2';
119 2         6 my $tpxxsd = 'http://www.garmin.com/xmlschemas/ActivityExtensionv2.xsd';
120 2         7 my $lxns = $tpxns;
121 2         5 my $lxxsd = $tpxxsd;
122 2         6 my (%xmllocation, @xmllocation);
123 2         8 $xmllocation{$tpxns} = $tpxxsd;
124 2         5 push @xmllocation, $tpxns, $tpxxsd;
125 2         7 $xmllocation{$fcns} = $fcxsd;
126 2         5 push @xmllocation, $fcns, $fcxsd;
127 2         9 $xmllocation{$lxns} = $lxxsd;
128 2         7 push @xmllocation, $lxns, $lxxsd;
129 2         6 $xmllocation{$tcdns} = $tcdxsd;
130 2         7 push @xmllocation, $tcdns, $tcdxsd;
131              
132 2         9 my $indent = ' ' x $indent_n;
133              
134 2         17 my $start = <
135            
136            
137             $indent
138             EOF
139              
140 2         7 my $end = <
141             $indent
142            
143             EOF
144              
145 2         8 $indent .= ' ' x $indent_n;
146              
147 2         37 my @with_ushort_value_def = ('sub' => [+{'name' => 'Value', 'format' => 'u'}]);
148              
149 2         203 my %activity_def = (
150             'name' => 'Activity',
151             'array' => 1,
152             'attr' => [+{'name' => 'Sport', 'format' => 's'}],
153             'sub' => [
154             +{'name' => 'Id', 'format' => 's'},
155             +{
156             'name' => 'Lap',
157             'array' => 1,
158             'attr' => [+{'name' => 'StartTime', 'format' => 's'}],
159             'sub' => [
160             +{'name' => 'TotalTimeSeconds', 'format' => $pf},
161             +{'name' => 'DistanceMeters', 'format' => $pf},
162             +{'name' => 'MaximumSpeed', 'format' => $pf},
163             +{'name' => 'Calories', 'format' => 'u'},
164             +{'name' => 'AverageHeartRateBpm', @with_ushort_value_def},
165             +{'name' => 'MaximumHeartRateBpm', @with_ushort_value_def},
166             +{'name' => 'Intensity', 'format' => 's'},
167             +{'name' => 'Cadence', 'format' => 'u'},
168             +{'name' => 'TriggerMethod', 'format' => 's'},
169             +{
170             'name' => 'Track',
171             'array' => 1,
172             'sub' => [
173             +{
174             'name' => 'Trackpoint',
175             'array' => 1,
176             'sub' => [
177             +{'name' => 'Time', 'format' => 's'},
178             +{
179             'name' => 'Position',
180             'sub' => [
181             +{'name' => 'LatitudeDegrees', 'format' => $pf},
182             +{'name' => 'LongitudeDegrees', 'format' => $pf},
183             ],
184             },
185             +{'name' => 'AltitudeMeters', 'format' => $pf},
186             +{'name' => 'DistanceMeters', 'format' => $pf},
187             +{'name' => 'HeartRateBpm', @with_ushort_value_def},
188             +{'name' => 'Cadence', 'format' => 'u'},
189             +{
190             'name' => 'Extensions',
191             'sub' => [
192             +{
193             'name' => 'TPX',
194             'attr' => [+{'name' => 'xmlns', 'fixed' => $tpxns}],
195             'sub' => [
196             +{'name' => 'Speed', 'format' => $pf},
197             +{'name' => 'Watts', 'format' => 'u'},
198             ],
199             },
200             ],
201             },
202             ],
203             },
204             ],
205             },
206              
207             +{
208             'name' => 'Extensions',
209             'sub' => [
210             +{
211             'name' => 'FatCalories',
212             'attr' => [+{'name' => 'xmlns', 'fixed' => $fcns}],
213             @with_ushort_value_def,
214             },
215             +{
216             'name' => 'LX',
217             'attr' => [+{'name' => 'xmlns', 'fixed' => $lxns}],
218             'sub' => [
219             +{'name' => 'AvgSpeed', 'format' => $pf},
220             +{'name' => 'MaxBikeCadence', 'format' => 'u'},
221             +{'name' => 'AvgWatts', 'format' => 'u'},
222             +{'name' => 'MaxWatts', 'format' => 'u'},
223             ],
224             },
225             ],
226             },
227             ],
228             },
229              
230             +{
231             'name' => 'Creator',
232             'attr' => [+{'name' => 'xsi:type', 'fixed' => 'Device_t'}],
233             'sub' => [
234             +{'name' => 'Name', 'format' => 's'},
235             +{'name' => 'UnitId', 'format' => 'u'},
236             +{'name' => 'ProductID', 'format' => 'u'},
237             +{'name' => 'Version',
238             'sub' => [
239             +{'name' => 'VersionMajor', 'format' => 's'},
240             +{'name' => 'VersionMinor', 'format' => 's'},
241             ],
242             },
243             ],
244             },
245             ],
246             );
247              
248 2         9 my @lap;
249 2         11 for my $beg_end (split /,/, $lap, -1) {
250 0 0       0 if ($beg_end =~ /-/) {
251 0         0 push @lap, $`, $'
252             } else {
253 0         0 push @lap, $beg_end, $beg_end
254             }
255             }
256              
257 2 50 33     11 if (@lap && $lap_start > 0) {
258 0         0 for (my $i = 0 ; $i < @lap ; ++$i) {
259 0 0       0 if ($lap[$i] =~ /^(\*|all)?$/i) {
260 0 0       0 $lap[$i] = $i % 2 ? $lap_max : 0
261             } else {
262 0         0 $lap[$i] -= $lap_start
263             }
264             }
265             }
266              
267             sub cmp_long {
268 0     0   0 my ($a, $b) = @_;
269              
270 0 0       0 if ($a < $b) {
    0          
271 0 0       0 if ($a - $b <= -180) {
272 0         0 1
273             } else {
274 0         0 -1
275             }
276             } elsif ($a > $b) {
277 0 0       0 if ($a - $b >= 180) {
278 0         0 -1
279             } else {
280 0         0 1
281             }
282             } else {
283 0         0 0
284             }
285             }
286              
287             sub tpmask_rect {
288 0     0   0 my ($lat, $lon, $lat_sw, $lon_sw, $lat_ne, $lon_ne) = @_;
289 0 0 0     0 $lat >= $lat_sw && $lat <= $lat_ne && &cmp_long($lon, $lon_sw) >= 0 && &cmp_long($lon, $lon_ne) <= 0
      0        
290             }
291              
292             sub tpmask_make {
293 4     4   11 my ($masks, $maskv) = @_;
294              
295 4         12 for my $mask (split /:|\s+/, $masks) {
296 0         0 my @v = split /,/, $mask;
297              
298 0 0       0 if (@v % 2) {
    0          
    0          
299 0         0 die "$mask: not a sequence of latitude and longitude pairs"
300             } elsif (@v < 4) {
301 0         0 die "$mask: \# of vertices < 2"
302             } elsif (@v > 4) {
303 0         0 die "$mask: sorry but arbitrary polygons are not implemented"
304             } else {
305             grep {
306 0         0 s/^\s+|\s+$//g
  0         0  
307             } @v;
308              
309 0 0       0 $v[0] > $v[2] and @v[0, 2] = @v[2, 0];
310 0 0       0 &cmp_long($v[1], $v[3]) > 0 and @v[1, 3] = @v[3, 1];
311 0         0 push @$maskv, [\&tpmask_rect, @v]
312             }
313             }
314             }
315              
316 2         4 my @tpmask;
317 2         11 &tpmask_make($tpmask, \@tpmask);
318              
319 2         4 my @tpfake;
320 2         7 &tpmask_make($tpfake, \@tpfake);
321              
322 2         14 my $memo = { 'tpv' => [], 'trackv' => [], 'lapv' => [], 'av' => [] };
323 2         31 my $fit = new Geo::FIT;
324              
325 2         7 $fit->use_gmtime(1);
326 2         12 $fit->numeric_date_time(0);
327 2         7 $fit->semicircles_to_degree(1);
328 2         21 $fit->without_unit(1);
329 2         27 $fit->mps_to_kph(0);
330              
331             sub cb_file_id {
332 2     2   9 my ($obj, $desc, $v, $memo) = @_;
333 2         4 my $file_type = $obj->value_cooked(@{$desc}{qw(t_type a_type I_type)}, $v->[$desc->{i_type}]);
  2         20  
334              
335 2 50       7 if ($file_type eq 'activity') {
336 2         6 1
337             } else {
338 0         0 $obj->error("$file_type: not an activity");
339             undef
340 0         0 }
341             }
342              
343             sub cb_device_info {
344 16     16   36 my ($obj, $desc, $v, $memo) = @_;
345              
346 16 100 66     50 if ($include_creator &&
347 16         104 $obj->value_cooked(@{$desc}{qw(t_device_index a_device_index I_device_index)}, $v->[$desc->{i_device_index}]) eq 'creator') {
348 4         18 my ($tname, $attr, $inval, $id) = (@{$desc}{qw(t_product a_product I_product)}, $v->[$desc->{i_product}]);
  4         21  
349 4         29 my $t_attr = $obj->switched($desc, $v, $attr->{switch});
350              
351 4 50       14 if (ref $t_attr eq 'HASH') {
352 4         7 $attr = $t_attr;
353             $tname = $attr->{type_name}
354 4         12 }
355              
356 4         10 my $ver = $obj->value_cooked(@{$desc}{qw(t_software_version a_software_version I_software_version)}, $v->[$desc->{i_software_version}]);
  4         25  
357 4         36 my ($major, $minor) = split /\./, $ver, 2;
358              
359             $memo->{Creator} = +{
360             'Name' => $obj->value_cooked($tname, $attr, $inval, $id),
361 4         15 'UnitId' => $v->[$desc->{i_serial_number}],
362             'ProductID' => $id,
363             'Version' => +{
364             'VersionMajor' => $major,
365             'VersionMinor' => $minor,
366             }
367             }
368             }
369             1
370 16         35 }
371              
372             sub cb_record {
373 414     414   909 my ($obj, $desc, $v, $memo) = @_;
374 414         628 my (%tp, $lat, $lon, $speed, $watts);
375              
376 414         1670 $tp{Time} = $obj->named_type_value($desc->{t_timestamp}, $v->[$desc->{i_timestamp}]);
377 414 100       1146 $memo->{id} = $tp{Time} if !defined $memo->{id};
378              
379             $lat = $obj->value_processed($v->[$desc->{i_position_lat}], $desc->{a_position_lat})
380 414 50 33     2908 if defined $desc->{i_position_lat} && $v->[$desc->{i_position_lat}] != $desc->{I_position_lat};
381              
382             $lon = $obj->value_processed($v->[$desc->{i_position_long}], $desc->{a_position_long})
383 414 50 33     2820 if defined $desc->{i_position_long} && $v->[$desc->{i_position_long}] != $desc->{I_position_long};
384              
385 414 50 33     3146 defined $lat and defined $lon and $tp{Position} = +{'LatitudeDegrees' => $lat, 'LongitudeDegrees' => $lon};
386              
387 414 50 33     2375 if (defined $desc->{i_enhanced_altitude} && $v->[$desc->{i_enhanced_altitude}] != $desc->{I_enhanced_altitude}) {
    50 33        
388             $tp{AltitudeMeters} = $obj->value_processed($v->[$desc->{i_enhanced_altitude}], $desc->{a_enhanced_altitude})
389 0         0 } elsif (defined $desc->{i_altitude} && $v->[$desc->{i_altitude}] != $desc->{I_altitude}) {
390             $tp{AltitudeMeters} = $obj->value_processed($v->[$desc->{i_altitude}], $desc->{a_altitude})
391 414         1259 }
392              
393             $tp{DistanceMeters} = $obj->value_processed($v->[$desc->{i_distance}], $desc->{a_distance})
394 414 50 33     2522 if defined $desc->{i_distance} && $v->[$desc->{i_distance}] != $desc->{I_distance};
395              
396 414 50 33     2510 if (defined $desc->{i_enhanced_speed} && $v->[$desc->{i_enhanced_speed}] != $desc->{I_enhanced_speed}) {
    50 33        
397             $speed = $obj->value_processed($v->[$desc->{i_enhanced_speed}], $desc->{a_enhanced_speed})
398 0         0 } elsif (defined $desc->{i_speed} && $v->[$desc->{i_speed}] != $desc->{I_speed}) {
399             $speed = $obj->value_processed($v->[$desc->{i_speed}], $desc->{a_speed})
400 414         1088 }
401              
402 414 50 33     2011 $tp{HeartRateBpm} = +{'Value' => $v->[$desc->{i_heart_rate}]} if defined $desc->{i_heart_rate} && $v->[$desc->{i_heart_rate}] != $desc->{I_heart_rate};
403 414 50 33     1035 $tp{Cadence} = $v->[$desc->{i_cadence}] if defined $desc->{i_cadence} && $v->[$desc->{i_cadence}] != $desc->{I_cadence};
404 414 50 33     958 $watts = $v->[$desc->{i_power}] * $pw_fix + $pw_fix_b if defined $desc->{i_power} && $v->[$desc->{i_power}] != $desc->{I_power};
405              
406 414 50 33     970 if (defined $speed || defined $watts) {
407 414         578 my %tpx;
408 414 50       1466 $tpx{Speed} = $speed if defined $speed;
409 414 50       783 $tpx{Watts} = $watts if defined $watts;
410 414         1973 $tp{Extensions} = +{'TPX' => \%tpx}
411             }
412              
413 414         694 my $miss;
414 414         964 for my $k (@tp_exclude) {
415 0         0 delete $tp{$k}
416             }
417 414         675 for my $k (@must) {
418 414 50       1135 defined $tp{$k} or ++$miss
419             }
420 414 50       794 push @{$memo->{tpv}}, \%tp if !$miss;
  414         1027  
421 414         1316 1
422             }
423              
424             sub track_end {
425 8     8   13 my $memo = shift;
426 8         12 my $ntps = @{$memo->{tpv}};
  8         17  
427              
428 8 100       44 if ($ntps) {
429 6         40 my %track = ('Trackpoint' => [@{$memo->{tpv}}]);
  6         199  
430              
431 6         15 @{$memo->{tpv}} = ();
  6         34  
432 6         17 $memo->{ntps} += $ntps;
433 6         12 push @{$memo->{trackv}}, \%track
  6         21  
434             }
435             }
436              
437             sub cb_event {
438 6     6   34 my ($obj, $desc, $v, $memo) = @_;
439 6         30 my $event = $obj->named_type_value($desc->{t_event}, $v->[$desc->{i_event}]);
440 6         23 my $event_type = $obj->named_type_value($desc->{t_event_type}, $v->[$desc->{i_event_type}]);
441              
442 6 100       21 if ($event_type eq 'stop_all') {
443 2         12 &track_end($memo)
444             }
445             1
446 6         14 }
447              
448 2         12 my %intensity = (
449             'active' => 'Active',
450             'rest' => 'Resting',
451             );
452              
453 2         12 my %lap_trigger = (
454             'manual' => 'Manual',
455             'distance' => 'Distance',
456             'time' => 'Time',
457             );
458              
459             sub cb_lap {
460 6     6   21 my ($obj, $desc, $v, $memo) = @_;
461 6         29 &track_end($memo);
462              
463 6 50       13 if (@{$memo->{trackv}}) {
  6         27  
464 6         14 my %lap = ('Track' => [@{$memo->{trackv}}]);
  6         42  
465              
466 6         105 @{$memo->{trackv}} = ();
  6         16  
467 6         128 $lap{'StartTime'} = $obj->named_type_value($desc->{t_start_time}, $v->[$desc->{i_start_time}]);
468 6         38 $lap{TotalTimeSeconds} = $obj->value_processed($v->[$desc->{i_total_timer_time}], $desc->{a_total_timer_time});
469              
470             $lap{DistanceMeters} = $obj->value_processed($v->[$desc->{i_total_distance}], $desc->{a_total_distance})
471 6 50 33     91 if defined $desc->{i_total_distance} && $v->[$desc->{i_total_distance}] != $desc->{I_total_distance};
472              
473 6 50 33     100 if (defined $desc->{i_enhanced_max_speed} && $v->[$desc->{i_enhanced_max_speed}] != $desc->{I_enhanced_max_speed}) {
    50 33        
474             $lap{MaximumSpeed} = $obj->value_processed($v->[$desc->{i_enhanced_max_speed}], $desc->{a_enhanced_max_speed})
475 0         0 } elsif (defined $desc->{i_max_speed} && $v->[$desc->{i_max_speed}] != $desc->{I_max_speed}) {
476             $lap{MaximumSpeed} = $obj->value_processed($v->[$desc->{i_max_speed}], $desc->{a_max_speed})
477 6         31 }
478              
479             $lap{Calories} = $v->[$desc->{i_total_calories}]
480 6 50 33     61 if defined $desc->{i_total_calories} && $v->[$desc->{i_total_calories}] != $desc->{I_total_calories};
481              
482 6 50 33     46 $lap{Cadence} = $v->[$desc->{i_avg_cadence}] if defined $desc->{i_avg_cadence} && $v->[$desc->{i_avg_cadence}] != $desc->{I_avg_cadence};
483              
484 6         16 my $intensity = $obj->value_cooked(@{$desc}{qw(t_intensity a_intensity I_intensity)}, $v->[$desc->{i_intensity}]);
  6         40  
485              
486 6 50       43 defined ($lap{Intensity} = $intensity{$intensity}) or $lap{Intensity} = 'Active';
487              
488 6         12 my $lap_trigger = $obj->value_cooked(@{$desc}{qw(t_lap_trigger a_lap_trigger I_lap_trigger)}, $v->[$desc->{i_lap_trigger}]);
  6         53  
489              
490 6 100       35 defined ($lap{TriggerMethod} = $lap_trigger{$lap_trigger}) or $lap{TriggerMethod} = 'Manual';
491              
492             $lap{AverageHeartRateBpm} = +{'Value' => $v->[$desc->{i_avg_heart_rate}]}
493 6 50 33     72 if defined $desc->{i_avg_heart_rate} && $v->[$desc->{i_avg_heart_rate}] != $desc->{I_avg_heart_rate};
494              
495             $lap{MaximumHeartRateBpm} = +{'Value' => $v->[$desc->{i_max_heart_rate}]}
496 6 50 33     44 if defined $desc->{i_max_heart_rate} && $v->[$desc->{i_max_heart_rate}] != $desc->{I_max_heart_rate};
497              
498 6         12 my (%x, %lx);
499             $x{FatCalories} = +{'Value' => $v->[$desc->{i_total_fat_calories}]}
500 6 50 33     75 if defined $desc->{i_total_fat_calories} && $v->[$desc->{i_total_fat_calories}] != $desc->{I_total_calories};
501              
502 6 50 33     88 if (defined $desc->{i_enhanced_avg_speed} && $v->[$desc->{i_enhanced_avg_speed}] != $desc->{I_enhanced_avg_speed}) {
    50 33        
503             $lx{AvgSpeed} = $obj->value_processed($v->[$desc->{i_enhanced_avg_speed}], $desc->{a_enhanced_avg_speed})
504 0         0 } elsif (defined $desc->{i_avg_speed} && $v->[$desc->{i_avg_speed}] != $desc->{I_avg_speed}) {
505             $lx{AvgSpeed} = $obj->value_processed($v->[$desc->{i_avg_speed}], $desc->{a_avg_speed})
506 6         35 }
507              
508 6 50 33     42 $lx{MaxBikeCadence} = $v->[$desc->{i_max_cadence}] if defined $desc->{i_max_cadence} && $v->[$desc->{i_max_cadence}] != $desc->{I_max_cadence};
509 6 50 33     38 $lx{AvgWatts} = $v->[$desc->{i_avg_power}] * $pw_fix + $pw_fix_b if defined $desc->{i_avg_power} && $v->[$desc->{i_avg_power}] != $desc->{I_avg_power};
510 6 50 33     39 $lx{MaxWatts} = $v->[$desc->{i_max_power}] * $pw_fix + $pw_fix_b if defined $desc->{i_max_power} && $v->[$desc->{i_max_power}] != $desc->{I_max_power};
511 6 50       29 %lx and $x{LX} = \%lx;
512 6 50       22 %x and $lap{Extensions} = \%x;
513 6         13 push @{$memo->{lapv}}, \%lap
  6         67  
514             }
515             1
516 6         21 }
517              
518 2         12 my %sport = (
519             'running' => 'Running',
520             'cycling' => 'Biking',
521             );
522              
523             sub cb_session {
524 2     2   7 my ($obj, $desc, $v, $memo) = @_;
525              
526 2 50       5 unless (@{$memo->{lapv}}) {
  2         12  
527 0 0       0 &cb_lap($obj, $desc, $v, $memo) || return undef
528             }
529              
530 2 50       4 if (@{$memo->{lapv}}) {
  2         8  
531 2         4 my %activity;
532              
533 2 50       12 defined($activity{'Sport'} = $sport{$obj->named_type_value($desc->{t_sport}, $v->[$desc->{i_sport}])}) or $activity{'Sport'} = 'Other';
534 2         10 $activity{Id} = $obj->named_type_value($desc->{t_start_time}, $v->[$desc->{i_start_time}]);
535 2         3 $activity{Lap} = [@{$memo->{lapv}}];
  2         9  
536 2         4 @{$memo->{lapv}} = ();
  2         5  
537 2 50       12 $activity{Creator} = $memo->{Creator} if defined $memo->{Creator};
538 2         19 push @{$memo->{av}}, \%activity
  2         6  
539             }
540              
541 2         5 delete $memo->{Creator};
542 2         4 1
543             }
544              
545             sub output {
546 4238     4238   6707 my ($datum, $def, $indent, $T) = @_;
547              
548 4238 100       5990 if (ref $datum eq 'ARRAY') {
549 14         26 for my $datum1 (@$datum) {
550 426         2482 &output($datum1, $def, $indent, $T)
551             }
552             } else {
553 4224         9329 $T->print("$indent<$def->{name}");
554 4224         21384 my $attrv = $def->{attr};
555              
556 4224 100       6435 if (ref $attrv eq 'ARRAY') {
557 430         690 for my $attr (@$attrv) {
558 430         461 my ($aname, $aformat, $afixed) = @{$attr}{qw(name format fixed)};
  430         895  
559              
560 430         1010 $T->print(" $aname=\"");
561              
562 430 100       2075 if (defined $afixed) {
    50          
563 422         701 $T->print($afixed)
564             } elsif (defined $aformat) {
565 8         49 $T->printf("%$aformat", $datum->{'' . $aname})
566             }
567 430         2139 $T->print("\"")
568             }
569             }
570              
571 4224         9174 $T->print(">");
572              
573 4224         17866 my ($sub, $format) = @{$def}{qw(sub format)};
  4224         7943  
574              
575 4224 100 66     10380 if (defined $format and $format ne '') {
    50          
576 2538         6026 $T->printf("%$format", $datum)
577             } elsif (ref $sub eq 'ARRAY') {
578 1686         3047 $T->print("\n");
579              
580 1686         7976 my $subindent = $indent . ' ' x $indent_n;
581 1686         1743 my $i;
582              
583 1686         2557 for ($i = 0 ; $i < @$sub ;) {
584 5094         18140 my $subdef = $sub->[$i++];
585 5094         9120 my $subdatum = $datum->{$subdef->{name}};
586 5094 100       9730 defined $subdatum and &output($subdatum, $subdef, $subindent, $T)
587             }
588 1686         8416 $T->print($indent)
589             }
590 4224         33529 $T->print("{name}>\n")
591             }
592             }
593              
594 2 50       11 $fit->data_message_callback_by_name('file_id', \&cb_file_id, $memo) or die $fit->error;
595 2 50       10 $fit->data_message_callback_by_name('device_info', \&cb_device_info, $memo) or die $fit->error;
596 2 50       10 $fit->data_message_callback_by_name('record', \&cb_record, $memo) or die $fit->error;
597 2 50       10 $fit->data_message_callback_by_name('event', \&cb_event, $memo) or die $fit->error;
598 2 50       8 $fit->data_message_callback_by_name('lap', \&cb_lap, $memo) or die $fit->error;
599 2 50       18 $fit->data_message_callback_by_name('session', \&cb_session, $memo) or die $fit->error;
600 2         23 $fit->file($from);
601 2 50       9 $fit->open || die $fit->error;
602              
603             sub dead {
604 0     0     my ($obj, $err) = @_;
605 0           my ($p, $fn, $l, $subr, $fit);
606              
607 0 0         $err = $obj->{error} if !defined $err;
608 0           (undef, $fn, $l) = caller(0);
609 0           ($p, undef, undef, $subr) = caller(1);
610 0           $obj->close;
611 0           die "$p::$subr\#$l\@$fn: $err\n"
612             }
613              
614 2         16 my ($fsize, $proto_ver, $prof_ver, $h_extra, $h_crc_expected, $h_crc_calculated) = $fit->fetch_header;
615              
616 2 50       15 defined $fsize || &dead($fit);
617              
618 2         20 my $protocol_version = $fit->protocol_version( $proto_ver );
619 2         12 my ($prof_major, $prof_minor) = $fit->profile_version( $prof_ver );
620              
621 2 50       11 if ($verbose) {
622 0         0 printf "File size: %lu, protocol version: %u, profile_version: %u.%02u\n", $fsize, $protocol_version, $prof_major, $prof_minor;
623              
624 0 0       0 if ($h_extra ne '') {
625 0         0 print "Hex dump of extra octets in the file header";
626              
627 0         0 my ($i, $n);
628 0         0 for ($i = 0, $n = length($h_extra) ; $i < $n ; ++$i) {
629 0 0       0 print "\n " if !($i % 16);
630 0 0       0 print ' ' if !($i % 4);
631 0         0 printf " %02x", ord(substr($h_extra, $i, 1))
632             }
633 0         0 print "\n"
634             }
635              
636 0 0       0 if (defined $h_crc_calculated) {
637 0         0 printf "File header CRC: expected=0x%04X, calculated=0x%04X\n", $h_crc_expected, $h_crc_calculated
638             }
639             }
640              
641 2         15 1 while $fit->fetch;
642 2 50       8 $fit->EOF || &dead($fit);
643              
644 2 50       9 if ($verbose) {
645 0         0 printf "CRC: expected=0x%04X, calculated=0x%04X\n", $fit->crc_expected, $fit->crc;
646 0         0 my $garbage_size = $fit->trailing_garbages;
647 0 0 0     0 print "Trailing $garbage_size octets garbages skipped\n" if defined $garbage_size and $garbage_size > 0
648             }
649              
650 2         13 $fit->close;
651              
652 2         97 my $av = $memo->{av};
653              
654 2 50       9 if (@$av) {
655 2         3 my ($i, $j);
656              
657 2         27 for ($i = $j = 0 ; $i < @$av ; ++$i) {
658 2         8 my $lv = $av->[$i]->{Lap};
659              
660 2 50       8 if (@lap) {
661 0         0 my ($p, $q, $r);
662              
663 0         0 for ($p = $q = 0 ; $p < @$lv ; ++$p) {
664 0         0 for ($r = 1 ; $r < @lap ; $r += 2) {
665 0 0 0     0 if ($p >= $lap[$r - 1] && $p <= $lap[$r]) {
666 0         0 $lv->[$q++] = $lv->[$p];
667             last
668 0         0 }
669             }
670             }
671 0         0 splice @$lv, $q
672             }
673 2 50       17 @$lv and $av->[$j++] = $av->[$i]
674             }
675 2         7 splice @$av, $j
676             }
677              
678             sub minus_long {
679 0     0     my ($a, $b);
680              
681 0           $a -= $b;
682              
683 0 0         if ($a < -180) {
    0          
684 0           $a += 360
685             } elsif ($a > 180) {
686 0           $a = 360 - $a
687             }
688             $a
689 0           }
690              
691 2 50 33     16 if (@$av && (@tpmask || @tpfake)) {
      33        
692 0         0 my ($i, $j, $tp_prev);
693              
694 0         0 for ($i = $j = 0 ; $i < @$av ; ++$i) {
695 0         0 my $lv = $av->[$i]->{Lap};
696 0         0 my ($p, $q);
697              
698 0         0 for ($p = $q = 0 ; $p < @$lv ; ++$p) {
699 0         0 my $trkv = $lv->[$p]->{Track};
700 0         0 my ($u, $v);
701              
702 0         0 for ($u = $v = 0 ; $u < @$trkv ; ++$u) {
703 0         0 my $tpv =$trkv->[$u]->{Trackpoint};
704 0         0 my ($r, $s);
705              
706 0         0 for ($r = $s = 0 ; $r < @$tpv ; ++$r) {
707 0         0 my $masked;
708              
709 0         0 for my $mask (@tpmask) {
710 0 0       0 if ($mask->[0]->(@{$tpv->[$r]->{Position}}{qw(LatitudeDegrees LongitudeDegrees)}, @$mask[1 .. $#$mask])) {
  0         0  
711 0         0 $memo->{ntps} -= 1;
712 0         0 $masked = 1;
713             last
714 0         0 }
715             }
716              
717 0 0       0 unless ($masked) {
718 0         0 my $tp_cur = $tpv->[$r]->{Position};
719 0         0 my ($cur_lat, $cur_long) = @$tp_cur{qw(LatitudeDegrees LongitudeDegrees)};
720              
721 0         0 for my $mask (@tpfake) {
722 0 0       0 if ($mask->[0]->($cur_lat, $cur_long, @$mask[1 .. $#$mask])) {
723 0         0 my $y;
724              
725 0 0       0 if (ref $tp_prev eq 'HASH') {
726 0         0 my ($prev_lat, $prev_long) = @$tp_prev{qw(LatitudeDegrees LongitudeDegrees)};
727 0         0 my $sq = ($prev_lat - $cur_lat) ** 2 + &minus_long($prev_long, $cur_long) ** 2;
728 0         0 my (@x, $x_lat, $x_long);
729              
730 0         0 for $x_lat (@$mask[3, 1]) {
731 0         0 my $x_sq = ($prev_lat - $x_lat) ** 2;
732              
733 0 0       0 if ($x_sq <= $sq) {
734 0         0 my $diff_long = sqrt($sq - $x_sq);
735              
736 0         0 $x_long = $prev_long + $diff_long;
737 0 0       0 $x_long >= 180 and $x_long -= 360;
738              
739 0 0       0 unless (&cmp_long($x_long, $mask->[2]) < 0) {
740 0 0       0 &cmp_long($x_long, $mask->[4]) <= 0 and push @x, [$x_lat, $x_long];
741 0         0 $x_long = $prev_long - $diff_long;
742 0 0       0 $x_long < -180 and $x_long += 360;
743 0 0 0     0 &cmp_long($x_long, $mask->[2]) >= 0 and &cmp_long($x_long, $mask->[4]) <= 0 and push @x, [$x_lat, $x_long]
744             }
745             }
746             }
747              
748 0         0 for $x_long (@$mask[2, 4]) {
749 0         0 my $x_sq = &minus_long($prev_long, $x_long) ** 2;
750              
751 0 0       0 if ($x_sq <= $sq) {
752 0         0 my $diff_lat = sqrt($sq - $x_sq);
753              
754 0         0 $x_lat = $prev_lat + $diff_lat;
755              
756 0 0       0 unless ($x_lat < $mask->[1]) {
757 0 0       0 $x_lat <= $mask->[3] and push @x, [$x_lat, $x_long];
758 0         0 $x_lat = $prev_lat - $diff_lat;
759 0 0 0     0 $x_lat >= $mask->[1] and $x_lat <= $mask->[3] and push @x, [$x_lat, $x_long]
760             }
761             }
762             }
763              
764 0 0       0 @x or die sprintf("prev=(%g, %g), cur=(%g, %g), mask=(%g, %g, %g, %g): \@x must not be empty.\n",
765             $prev_lat, $prev_long, $cur_lat, $cur_long, @$mask[1 .. 4]);
766              
767 0         0 my $y_sq;
768              
769 0         0 $y = shift @x;
770 0         0 $y_sq = ($y->[0] - $cur_lat) ** 2 + &minus_long($y->[1], $cur_long) ** 2;
771              
772 0 0       0 if ($y_sq > 0) {
773 0         0 while (@x) {
774 0         0 my $x_sq = ($x[0]->[0] - $cur_lat) ** 2 + &minus_long($x[0]->[1], $cur_long) ** 2;
775              
776 0 0       0 $x_sq < $y_sq and ($y, $y_sq) = ($x[0], $x_sq);
777             shift @x
778 0         0 }
779             }
780             } else {
781             ($y) = sort {
782 0         0 (($a->[0] - $cur_lat) ** 2 + &minus_long($a->[1], $cur_long) ** 2)
  0         0  
783             cmp (($b->[0] - $cur_lat) ** 2 + &minus_long($b->[1], $cur_long) ** 2)
784             } ([$cur_lat, $mask->[4]], [$cur_lat, $mask->[2]], [$mask->[1], $cur_long], [$mask->[3], $cur_long])
785             }
786              
787 0         0 @$tp_cur{qw(LatitudeDegrees LongitudeDegrees)} = @$y;
788             last
789 0         0 }
790             }
791 0         0 $tp_prev = $tp_cur;
792 0         0 $tpv->[$s++] = $tpv->[$r]
793             }
794             }
795 0         0 splice @$tpv, $s;
796 0 0       0 @$tpv and $trkv->[$v++] = $trkv->[$u]
797             }
798 0         0 splice @$trkv, $v;
799 0 0       0 @$trkv and $lv->[$q++] = $lv->[$p]
800             }
801 0         0 splice @$lv, $q;
802 0 0       0 @$lv and $av->[$j++] = $av->[$i]
803             }
804 0         0 splice @$av, $j
805             }
806              
807 2 50       6 if (@$av) {
808 2         27 my $T = new FileHandle "> $to";
809              
810 2 50       604 defined $T || &dead($fit, "new FileHandle \"> $to\": $!");
811 2         14 $T->print($start);
812              
813 2         47 my $skip;
814              
815 2 50 33     12 if ($tplimit > 0 && ($skip = $memo->{ntps} / $tplimit) > 1) {
816 0         0 for my $a (@$av) {
817 0         0 for my $l (@{$a->{Lap}}) {
  0         0  
818 0         0 for my $t (@{$l->{Track}}) {
  0         0  
819 0         0 my $tpv = $t->{Trackpoint};
820 0         0 my ($j, @mv);
821              
822 0 0 0     0 if ($tplimit_smart && defined $tpv->[0]->{AltitudeMeters}) {
823 0         0 for (my $i = 1 ; $i < $#$tpv ;) {
824 0         0 my $updown;
825              
826 0         0 for ($j = $i + 1 ; $j < @$tpv ; ++$j) {
827 0 0       0 if (defined $tpv->[$j]->{AltitudeMeters}) {
828 0 0       0 if (($updown = $tpv->[$j]->{AltitudeMeters} - $tpv->[$i]->{AltitudeMeters})) {
829             last
830 0         0 }
831             }
832             }
833              
834 0 0       0 if ($updown) {
835 0         0 my $k;
836              
837 0         0 for ($k = $j + 1 ; $k < @$tpv ; ++$k) {
838 0 0       0 if (defined $tpv->[$k]->{AltitudeMeters}) {
839 0 0       0 if (($tpv->[$k]->{AltitudeMeters} - $tpv->[$j]->{AltitudeMeters}) / $updown < 0) {
840             last
841 0         0 }
842             }
843             }
844              
845 0 0 0     0 if ($k < @$tpv && $k - $i > $skip) {
846 0         0 push @mv, $k
847             }
848 0         0 $i = $j
849             } else {
850             last
851 0         0 }
852             }
853             }
854 0         0 push @mv, $#$tpv + 1;
855              
856 0         0 for (my $i = $j = 1 ; @mv ;) {
857 0         0 my $m = shift @mv;
858 0         0 my $start = $i;
859 0         0 my $count;
860              
861 0         0 for ($count = 0 ; $i < $m ; ++$j, ++$count) {
862 0         0 my $next = $start + int($count * $skip);
863 0         0 my ($k, $wsum, $wn, $csum, $cn);
864              
865 0   0     0 for ($k = $i ; $k < $next && $k < $m ; ++$k) {
866 0         0 my ($x, $tpx, $w);
867              
868 0 0 0     0 if (defined ($x = $tpv->[$k]->{Extensions}) &&
      0        
869             defined ($tpx = $x->{TPX}) &&
870             defined ($w = $tpx->{Watts})) {
871 0         0 $wsum += $w;
872 0         0 ++$wn
873             }
874              
875 0 0       0 if (defined $tpv->[$k]->{Cadence}) {
876 0         0 $csum += $tpv->[$k]->{Cadence};
877 0         0 ++$cn
878             }
879             }
880              
881 0         0 $tpv->[$j] = $tpv->[$k - 1];
882 0 0       0 $tpv->[$j]->{Extensions}->{TPX}->{Watts} = $wsum / $wn if $wn > 0;
883 0 0       0 $tpv->[$j]->{Cadence} = $csum / $cn if $cn > 0;
884 0         0 $i = $k
885             }
886             }
887 0 0       0 $j < @$tpv and splice @$tpv, $j
888             }
889             }
890             }
891             }
892              
893 2         7 for my $a (@$av) {
894 2         12 &output($a, \%activity_def, $indent, $T)
895             }
896 2         60 $T->print($end);
897 2         23 $T->close
898             }
899              
900             =head1 DEPENDENCIES
901              
902             L
903              
904             =head1 SEE ALSO
905              
906             L
907              
908             =head1 BUGS AND LIMITATIONS
909              
910             No bugs have been reported.
911              
912             Please report any bugs or feature requests to C, or through the web interface at L.
913              
914             =head1 AUTHOR
915              
916             Originally written by Kiyokazu Suto C<< suto@ks-and-ks.ne.jp >>.
917              
918             This version is maintained by Patrick Joly C<< >>.
919              
920             Please visit the project page at: L.
921              
922             =head1 VERSION
923              
924             1.13
925              
926             =head1 LICENSE AND COPYRIGHT
927              
928             Copyright 2022, Patrick Joly C<< patjol@cpan.org >>. All rights reserved.
929              
930             Copyright 2016-2022, Kiyokazu Suto C<< suto@ks-and-ks.ne.jp >>. All rights reserved.
931              
932             This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L.
933              
934             =head1 DISCLAIMER OF WARRANTY
935              
936             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.
937              
938             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.
939              
940             =cut
941              
942             1;
943