#! /usr/bin/perl -s

use FindBin;
use lib $FindBin::Bin;
use POSIX qw(strftime);
use IO::Handle;
use FileHandle;
use Time::Local;

BEGIN {
  STDERR->autoflush(1);

  ($me = $0) =~ s%^.*/|-(old|new)$%%g if !defined $me;
  $conf = $ENV{HOME} . '/.' . $me . '.pl' if !defined $conf;

  local *C;
  my $expr;

  if (open(C, "< $conf")) {
    {local $/; $expr = <C>;}
    close(C);
  }

  eval $expr if $expr ne '';
}

use Garmin::FIT;

$debug = 0 if !defined $debug;
$verbose = 0 if !defined $verbose;
$indent_step = 2 if !defined $indent_step;
$pw_fix = 1 if !defined $pw_fix;
$pw_fix_b = 0 if !defined $pw_fix_b;
$tplimit = 0 if !defined $tplimit;
$tplimit_smart = 1 if !defined $tplimit_smart;
$must = 'Time' if !defined $must;
$double_precision = 7 if !defined $double_precision;
$include_creator = 1 if !defined $include_creator;
$tcdns = 'http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2' if !defined $tcdns;
$tcdxsd = 'http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd' if !defined $tcdxsd;
$fcns = 'http://www.garmin.com/xmlschemas/FatCalories/v1' if !defined $fcns;
$fcxsd = 'http://www.garmin.com/xmlschemas/fatcalorieextensionv1.xsd' if !defined $fcxsd;
$tpxns = 'http://www.garmin.com/xmlschemas/ActivityExtension/v2' if !defined $tpxns;
$tpxxsd = 'http://www.garmin.com/xmlschemas/ActivityExtensionv2.xsd' if !defined $tpxxsd;
$lxns = $tpxns if !defined $lxns;
$lxxsd = $tpxxsd if !defined $lxxsd;
$lap = '' if !defined $lap;
$lap_start = 0 if !defined $lap_start;
$lap_max = ~(~0 << 16) - 1 if !defined $lap_max;
$tpmask = '' if !defined $tpmask;
$tpexclude = '' if !defined $tpexclude;
$show_version = 0 if !defined $show_version;

my $version = "0.11";

if ($show_version) {
  print $version, "\n";
  exit;
}

my @must = split /,/, $must;
my @tpexclude = split /,/, $tpexclude;
my ($from, $to) = qw(- -);

if (@ARGV) {
  $from = shift @ARGV;
  @ARGV and $to = shift @ARGV;
}

my (%xmllocation, @xmllocation);

if (!defined $xmlns{$tpxns}) {
  $xmlns{$tpxns} = $tpxxsd;
  push @xmllocation, $tpxns, $tpxxsd;
}

if (!defined $xmlns{$fcns}) {
  $xmlns{$fcns} = $fcxsd;
  push @xmllocation, $fcns, $fcxsd;
}

if (!defined $xmlns{$lxns}) {
  $xmlns{$lxns} = $lxxsd;
  push @xmllocation, $lxns, $lxxsd;
}

if (!defined $xmlns{$tcdns}) {
  $xmlns{$tcdns} = $tcdxsd;
  push @xmllocation, $tcdns, $tcdxsd;
}

my $indent = ' ' x $indent_step;

my $start = <<EOF;
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<TrainingCenterDatabase xmlns="$tcdns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="@xmllocation">
$indent<Activities>
EOF

my $end = <<EOF;
$indent</Activities>
</TrainingCenterDatabase>
EOF

$indent .= ' ' x $indent_step;

my $pf = $double_precision eq '' ? 'g' : '.' . $double_precision . 'f';
my @with_ushort_value_def = ('sub' => [+{'name' => 'Value', 'format' => 'u'}]);

my %activity_def =
  (
   'name' => 'Activity',
   'array' => 1,
   'attr' => [+{'name' => 'Sport', 'format' => 's'}],

   'sub' => [
     +{'name' => 'Id', 'format' => 's'},

     +{
       'name' => 'Lap',
       'array' => 1,
       'attr' => [+{'name' => 'StartTime', 'format' => 's'}],

       'sub' => [
	 +{'name' => 'TotalTimeSeconds', 'format' => $pf},
	 +{'name' => 'DistanceMeters', 'format' => $pf},
	 +{'name' => 'MaximumSpeed', 'format' => $pf},
	 +{'name' => 'Calories', 'format' => 'u'},
	 +{'name' => 'AverageHeartRateBpm', @with_ushort_value_def},
	 +{'name' => 'MaximumHeartRateBpm', @with_ushort_value_def},
	 +{'name' => 'Intensity', 'format' => 's'},
	 +{'name' => 'Cadence', 'format' => 'u'},
	 +{'name' => 'TriggerMethod', 'format' => 's'},

	 +{
	   'name' => 'Track',
	   'array' => 1,

	   'sub' => [
	     +{
	       'name' => 'Trackpoint',
	       'array' => 1,

	       'sub' => [
		 +{'name' => 'Time', 'format' => 's'},

		 +{
		   'name' => 'Position',

		   'sub' => [
		     +{'name' => 'LatitudeDegrees', 'format' => $pf},
		     +{'name' => 'LongitudeDegrees', 'format' => $pf},
		   ],
		 },

		 +{'name' => 'AltitudeMeters', 'format' => $pf},
		 +{'name' => 'DistanceMeters', 'format' => $pf},
		 +{'name' => 'HeartRateBpm', @with_ushort_value_def},
		 +{'name' => 'Cadence', 'format' => 'u'},

		 +{
		   'name' => 'Extensions',

		   'sub' => [
		     +{
		       'name' => 'TPX',
		       'attr' => [+{'name' => 'xmlns', 'fixed' => $tpxns}],

		       'sub' => [
			 +{'name' => 'Speed', 'format' => $pf},
			 +{'name' => 'Watts', 'format' => 'u'},
		       ],
		     },
		   ],
		 },
	       ],
	     },
	   ],
	 },

	 +{
	   'name' => 'Extensions',

	   'sub' => [
	     +{
	       'name' => 'FatCalories',
	       'attr' => [+{'name' => 'xmlns', 'fixed' => $fcns}],
	       @with_ushort_value_def,
	     },

	     +{
	       'name' => 'LX',
	       'attr' => [+{'name' => 'xmlns', 'fixed' => $lxns}],

	       'sub' => [
		 +{'name' => 'AvgSpeed', 'format' => $pf},
		 +{'name' => 'MaxBikeCadence', 'format' => 'u'},
		 +{'name' => 'AvgWatts', 'format' => 'u'},
		 +{'name' => 'MaxWatts', 'format' => 'u'},
	       ],
	     },
	   ],
	 },
       ],
     },

     +{
       'name' => 'Creator',
       'attr' => [+{'name' => 'xsi:type', 'fixed' => 'Device_t'}],

       'sub' => [
	 +{'name' => 'Name', 'format' => 's'},
	 +{'name' => 'UnitId', 'format' => 'u'},
	 +{'name' => 'ProductID', 'format' => 'u'},

	 +{'name' => 'Version',
	   'sub' => [
	     +{'name' => 'VersionMajor', 'format' => 's'},
	     +{'name' => 'VersionMinor', 'format' => 's'},
	   ],
	 },
       ],
     },
   ],
   );

my (@lap, $beg_end);

foreach $beg_end (split /,/, $lap, -1) {
  if ($beg_end =~ /-/) {
    push @lap, $`, $';
  }
  else {
    push @lap, $beg_end, $beg_end;
  }
}

if (@lap && $lap_start > 0) {
  for ($i = 0 ; $i < @lap ; ++$i) {
    if ($lap[$i] =~ /^(\*|all)?$/i) {
      $lap[$i] = $i % 2 ? $lap_max : 0;
    }
    else {
      $lap[$i] -= $lap_start;
    }
  }
}

my (@tpmask, $mask);

sub cmp_lon {
  my ($a, $b) = @_;

  if ($a < $b) {
    if ($a - $b <= -180) {
      1;
    }
    else {
      -1;
    }
  }
  elsif ($a > $b) {
    if ($a - $b >= 180) {
      -1;
    }
    else {
      1;
    }
  }
  else {
    0;
  }
}

sub tpmask_rect {
  my ($lat, $lon, $lat_sw, $lon_sw, $lat_ne, $lon_ne) = @_;

  $lat >= $lat_sw && $lat <= $lat_ne && &cmp_lon($lon, $lon_sw) >= 0 && &cmp_lon($lon, $lon_ne) <= 0;
}

foreach $mask (split /:|\s+/, $tpmask) {
  my @v = split /,/, $mask;

  if (@v % 2) {
    die "$mask: not a sequence of latitude and longitude pairs";
  }
  elsif (@v < 4) {
    die "$mask: \# of vertices < 2";
  }
  elsif (@v > 4) {
    die "$mask: sorry but arbitrary polygons are not implemented yet";
  }
  else {
    grep {
      s/^\s+|\s+$//g;
    } @v;

    $v[0] > $v[2] and @v[0, 2] = @v[2, 0];
    &cmp_lon($v[1], $v[3]) > 0 and @v[1, 3] = @v[3, 1];
    push @tpmask, [\&tpmask_rect, @v];
  }
}

my %memo = ('tpv' => [], 'trackv' => [], 'lapv' => [], 'av' => []);
my $fit = new Garmin::FIT;

$fit->use_gmtime(1);
$fit->numeric_date_time(0);
$fit->semicircles_to_degree(1);
$fit->without_unit(1);
$fit->mps_to_kph(0);

sub cb_file_id {
  my ($obj, $desc, $v, $memo) = @_;
  my $file_type = $obj->value_cooked(@{$desc}{qw(t_type a_type I_type)}, $v->[$desc->{i_type}]);

  if ($file_type eq 'activity') {
    1;
  }
  else {
    $obj->error("$file_type: not an activity");
    undef;
  }
}

sub cb_device_info {
  my ($obj, $desc, $v, $memo) = @_;

  if ($include_creator &&
      $obj->value_cooked(@{$desc}{qw(t_device_index a_device_index I_device_index)}, $v->[$desc->{i_device_index}]) eq 'creator') {
    my ($tname, $attr, $inval, $id) = (@{$desc}{qw(t_product a_product I_product)}, $v->[$desc->{i_product}]);
    my $t_attr = $obj->switched($desc, $v, $attr->{switch});

    if (ref $t_attr eq 'HASH') {
      $attr = $t_attr;
      $tname = $attr->{type_name};
    }

    my $ver = $obj->value_cooked(@{$desc}{qw(t_software_version a_software_version I_software_version)}, $v->[$desc->{i_software_version}]);
    my ($major, $minor) = split /\./, $ver, 2;

    $memo->{Creator} = +{
      'Name' => $obj->value_cooked($tname, $attr, $inval, $id),
      'UnitId' => $v->[$desc->{i_serial_number}],
      'ProductID' => $id,

      'Version' => +{
	'VersionMajor' => $major,
	'VersionMinor' => $minor,
      }
    };
  }

  1;
}

sub cb_record {
  my ($obj, $desc, $v, $memo) = @_;
  my (%tp, $lat, $lon, $speed, $watts);

  $tp{Time} = $obj->named_type_value($desc->{t_timestamp}, $v->[$desc->{i_timestamp}]);
  $memo->{id} = $tp{Time} if !defined $memo->{id};

  $lat = $obj->value_processed($v->[$desc->{i_position_lat}], $desc->{a_position_lat})
    if defined $desc->{i_position_lat} && $v->[$desc->{i_position_lat}] != $desc->{I_position_lat};

  $lon = $obj->value_processed($v->[$desc->{i_position_long}], $desc->{a_position_long})
    if defined $desc->{i_position_long} && $v->[$desc->{i_position_long}] != $desc->{I_position_long};

  defined $lat and defined $lon and $tp{Position} = +{'LatitudeDegrees' => $lat, 'LongitudeDegrees' => $lon};

  if (defined $desc->{i_enhanced_altitude} && $v->[$desc->{i_enhanced_altitude}] != $desc->{I_enhanced_altitude}) {
    $tp{AltitudeMeters} = $obj->value_processed($v->[$desc->{i_enhanced_altitude}], $desc->{a_enhanced_altitude})
  }
  elsif (defined $desc->{i_altitude} && $v->[$desc->{i_altitude}] != $desc->{I_altitude}) {
    $tp{AltitudeMeters} = $obj->value_processed($v->[$desc->{i_altitude}], $desc->{a_altitude})
  }

  $tp{DistanceMeters} = $obj->value_processed($v->[$desc->{i_distance}], $desc->{a_distance})
    if defined $desc->{i_distance} && $v->[$desc->{i_distance}] != $desc->{I_distance};

  if (defined $desc->{i_enhanced_speed} && $v->[$desc->{i_enhanced_speed}] != $desc->{I_enhanced_speed}) {
    $speed = $obj->value_processed($v->[$desc->{i_enhanced_speed}], $desc->{a_enhanced_speed})
  }
  elsif (defined $desc->{i_speed} && $v->[$desc->{i_speed}] != $desc->{I_speed}) {
    $speed = $obj->value_processed($v->[$desc->{i_speed}], $desc->{a_speed})
  }

  $tp{HeartRateBpm} = +{'Value' => $v->[$desc->{i_heart_rate}]} if defined $desc->{i_heart_rate} && $v->[$desc->{i_heart_rate}] != $desc->{I_heart_rate};
  $tp{Cadence} = $v->[$desc->{i_cadence}] if defined $desc->{i_cadence} && $v->[$desc->{i_cadence}] != $desc->{I_cadence};
  $watts = $v->[$desc->{i_power}] * $pw_fix + $pw_fix_b if defined $desc->{i_power} && $v->[$desc->{i_power}] != $desc->{I_power};

  if (defined $speed || defined $watts) {
    my %tpx;

    $tpx{Speed} = $speed if defined $speed;
    $tpx{Watts} = $watts if defined $watts;
    $tp{Extensions} = +{'TPX' => \%tpx};
  }

  my ($miss, $k);

  foreach $k (@tpexclude) {
    delete $tp{$k};
  }

  foreach $k (@must) {
    defined $tp{$k} or ++$miss;
  }

  push @{$memo->{tpv}}, \%tp if !$miss;
  1;
}

sub track_end {
  my $memo = shift;
  my $ntps = @{$memo->{tpv}};

  if ($ntps) {
    my %track = ('Trackpoint' => [@{$memo->{tpv}}]);

    @{$memo->{tpv}} = ();
    $memo->{ntps} += $ntps;
    push @{$memo->{trackv}}, \%track;
  }
}

sub cb_event {
  my ($obj, $desc, $v, $memo) = @_;
  my $event = $obj->named_type_value($desc->{t_event}, $v->[$desc->{i_event}]);
  my $event_type = $obj->named_type_value($desc->{t_event_type}, $v->[$desc->{i_event_type}]);

  if ($event_type eq 'stop_all') {
    &track_end($memo);
  }

  1;
}

my %intensity =
  (
   'active' => 'Active',
   'rest' => 'Resting',
   );

my %lap_trigger =
  (
   'manual' => 'Manual',
   'distance' => 'Distance',
   'time' => 'Time',
   );

sub cb_lap {
  my ($obj, $desc, $v, $memo) = @_;

  &track_end($memo);

  if (@{$memo->{trackv}}) {
    my %lap = ('Track' => [@{$memo->{trackv}}]);

    @{$memo->{trackv}} = ();
    $lap{'<a>StartTime'} = $obj->named_type_value($desc->{t_start_time}, $v->[$desc->{i_start_time}]);
    $lap{TotalTimeSeconds} = $obj->value_processed($v->[$desc->{i_total_timer_time}], $desc->{a_total_timer_time});

    $lap{DistanceMeters} = $obj->value_processed($v->[$desc->{i_total_distance}], $desc->{a_total_distance})
      if defined $desc->{i_total_distance} && $v->[$desc->{i_total_distance}] != $desc->{I_total_distance};

    if (defined $desc->{i_enhanced_max_speed} && $v->[$desc->{i_enhanced_max_speed}] != $desc->{I_enhanced_max_speed}) {
      $lap{MaximumSpeed} = $obj->value_processed($v->[$desc->{i_enhanced_max_speed}], $desc->{a_enhanced_max_speed})
    }
    elsif (defined $desc->{i_max_speed} && $v->[$desc->{i_max_speed}] != $desc->{I_max_speed}) {
      $lap{MaximumSpeed} = $obj->value_processed($v->[$desc->{i_max_speed}], $desc->{a_max_speed})
    }

    $lap{Calories} = $v->[$desc->{i_total_calories}]
      if defined $desc->{i_total_calories} && $v->[$desc->{i_total_calories}] != $desc->{I_total_calories};

    $lap{Cadence} = $v->[$desc->{i_avg_cadence}] if defined $desc->{i_avg_cadence} && $v->[$desc->{i_avg_cadence}] != $desc->{I_avg_cadence};

    my $intensity = $obj->value_cooked(@{$desc}{qw(t_intensity a_intensity I_intensity)}, $v->[$desc->{i_intensity}]);

    defined ($lap{Intensity} = $intensity{$intensity}) or $lap{Intensity} = 'Active';

    my $lap_trigger = $obj->value_cooked(@{$desc}{qw(t_lap_trigger a_lap_trigger I_lap_trigger)}, $v->[$desc->{i_lap_trigger}]);

    defined ($lap{TriggerMethod} = $lap_triger{$lap_triger}) or $lap{TriggerMethod} = 'Manual';

    $lap{AverageHeartRateBpm} = +{'Value' => $v->[$desc->{i_avg_heart_rate}]}
      if defined $desc->{i_avg_heart_rate} && $v->[$desc->{i_avg_heart_rate}] != $desc->{I_avg_heart_rate};

    $lap{MaximumHeartRateBpm} = +{'Value' => $v->[$desc->{i_max_heart_rate}]}
      if defined $desc->{i_max_heart_rate} && $v->[$desc->{i_max_heart_rate}] != $desc->{I_max_heart_rate};

    my (%x, %lx);

    $x{FatCalories} = +{'Value' => $v->[$desc->{i_total_fat_calories}]}
      if defined $desc->{i_total_fat_calories} && $v->[$desc->{i_total_fat_calories}] != $desc->{I_total_calories};

    if (defined $desc->{i_enhanced_avg_speed} && $v->[$desc->{i_enhanced_avg_speed}] != $desc->{I_enhanced_avg_speed}) {
      $lx{AvgSpeed} = $obj->value_processed($v->[$desc->{i_enhanced_avg_speed}], $desc->{a_enhanced_avg_speed})
    }
    elsif (defined $desc->{i_avg_speed} && $v->[$desc->{i_avg_speed}] != $desc->{I_avg_speed}) {
      $lx{AvgSpeed} = $obj->value_processed($v->[$desc->{i_avg_speed}], $desc->{a_avg_speed})
    }

    $lx{MaxBikeCadence} = $v->[$desc->{i_max_cadence}] if defined $desc->{i_max_cadence} && $v->[$desc->{i_max_cadence}] != $desc->{I_max_cadence};
    $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};
    $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};
    %lx and $x{LX} = \%lx;
    %x and $lap{Extensions} = \%x;
    push @{$memo->{lapv}}, \%lap;
  }

  1;
}

my %sport =
  (
   'running' => 'Running',
   'cycling' => 'Biking',
   );

sub cb_session {
  my ($obj, $desc, $v, $memo) = @_;

  unless (@{$memo->{lapv}}) {
    &cb_lap($obj, $desc, $v, $memo) || return undef;
  }

  if (@{$memo->{lapv}}) {
    my %activity;

    defined($activity{'<a>Sport'} = $sport{$obj->named_type_value($desc->{t_sport}, $v->[$desc->{i_sport}])}) or $activity{'<a>Sport'} = 'Other';
    $activity{Id} = $obj->named_type_value($desc->{t_start_time}, $v->[$desc->{i_start_time}]);
    $activity{Lap} = [@{$memo->{lapv}}];
    @{$memo->{lapv}} = ();
    $activity{Creator} = $memo->{Creator} if defined $memo->{Creator};
    push @{$memo->{av}}, \%activity;
  }

  delete $memo->{Creator};
  1;
}

sub output {
  my ($datum, $def, $indent, $T) = @_;

  if (ref $datum eq 'ARRAY') {
    my $datum1;

    foreach $datum1 (@$datum) {
      &output($datum1, $def, $indent, $T);
    }
  }
  else {
    $T->print("$indent<$def->{name}");

    my $attrv = $def->{attr};

    if (ref $attrv eq 'ARRAY') {
      my $attr;

      foreach $attr (@$attrv) {
	my ($aname, $aformat, $afixed) = @{$attr}{qw(name format fixed)};

	$T->print(" $aname=\"");

	if (defined $afixed) {
	  $T->print($afixed);
	}
	elsif (defined $aformat) {
	  $T->printf("%$aformat", $datum->{'<a>' . $aname});
	}

	$T->print("\"");
      }
    }

    $T->print(">");

    my ($sub, $format) = @{$def}{qw(sub format)};

    if ($format ne '') {
      $T->printf("%$format", $datum);
    }
    elsif (ref $sub eq 'ARRAY') {
      $T->print("\n");

      my $subindent = $indent . ' ' x $indent_step;
      my $i;

      for ($i = 0 ; $i < @$sub ;) {
	my $subdef = $sub->[$i++];
	my $subdatum = $datum->{$subdef->{name}};

	defined $subdatum and &output($subdatum, $subdef, $subindent, $T);
      }

      $T->print($indent);
    }

    $T->print("</$def->{name}>\n");
  }
}

$fit->data_message_callback_by_name('file_id', \&cb_file_id, \%memo) || die $fit->error;
$fit->data_message_callback_by_name('device_info', \&cb_device_info, \%memo) || die $fit->error;
$fit->data_message_callback_by_name('record', \&cb_record, \%memo) || die $fit->error;
$fit->data_message_callback_by_name('event', \&cb_event, \%memo) || die $fit->error;
$fit->data_message_callback_by_name('lap', \&cb_lap, \%memo) || die $fit->error;
$fit->data_message_callback_by_name('session', \&cb_session, \%memo) || die $fit->error;
$fit->file($from);
$fit->open || die $fit->error;

sub dead {
  my ($obj, $err) = @_;
  my ($p, $fn, $l, $subr, $fit);

  $err = $obj->{error} if !defined $err;
  (undef, $fn, $l) = caller(0);
  ($p, undef, undef, $subr) = caller(1);
  $obj->close;
  die "$p::$subr\#$l\@$fn: $err\n";
}

my ($fsize, $proto_ver, $prof_ver, $h_extra, $h_crc_expected, $h_crc_calculated) = $fit->fetch_header;

defined $fsize || &dead($fit);

my ($proto_major, $proto_minor) = $fit->protocol_version_major($proto_ver);
my ($prof_major, $prof_minor) = $fit->profile_version_major($prof_ver);

if ($verbose) {
  printf "File size: %lu, protocol version: %u.%02u, profile_verion: %u.%02u\n", $fsize, $proto_major, $proto_minor, $prof_major, $prof_minor;

  if ($h_extra ne '') {
    print "Hex dump of extra octets in the file header";

    my ($i, $n);

    for ($i = 0, $n = length($h_extra) ; $i < $n ; ++$i) {
      print "\n  " if !($i % 16);
      print ' ' if !($i % 4);
      printf " %02x", ord(substr($h_extra, $i, 1));
    }

    print "\n";
  }

  if (defined $h_crc_calculated) {
    printf "File header CRC: expected=0x%04X, calculated=0x%04X\n", $h_crc_expected, $h_crc_calculated;
  }
}

1 while $fit->fetch;
$fit->EOF || &dead($fit);

if ($verbose) {
  printf "CRC: expected=0x%04X, calculated=0x%04X\n", $fit->crc_expected, $fit->crc;

  my $garbage_size = $fit->trailing_garbages;

  print "Trailing $garbage_size octets garbages skipped\n" if $garbage_size > 0;
}

$fit->close;

my $av = $memo{av};

if (@$av) {
  my ($i, $j);

  for ($i = $j = 0 ; $i < @$av ; ++$i) {
    my $lv = $av->[$i]->{Lap};

    if (@lap) {
      my ($p, $q, $r);

      for ($p = $q = 0 ; $p < @$lv ; ++$p) {
	for ($r = 1 ; $r < @lap ; $r += 2) {
	  if ($p >= $lap[$r - 1] && $p <= $lap[$r]) {
	    $lv->[$q++] = $lv->[$p];
	    last;
	  }
	}
      }

      splice @$lv, $q;
    }

    @$lv and $av->[$j++] = $av->[$i];
  }

  splice @$av, $j;
}

if (@$av && @tpmask) {
  my ($i, $j);

  for ($i = $j = 0 ; $i < @$av ; ++$i) {
    my $lv = $av->[$i]->{Lap};
    my ($p, $q);

    for ($p = $q = 0 ; $p < @$lv ; ++$p) {
      my $trkv = $lv->[$p]->{Track};
      my ($u, $v);

      for ($u = $v = 0 ; $u < @$trkv ; ++$u) {
	my $tpv =$trkv->[$u]->{Trackpoint};
	my ($r, $s);

	for ($r = $s = 0 ; $r < @$tpv ; ++$r) {
	  my ($mask, $masked);

	  foreach $mask (@tpmask) {
	    if ($mask->[0]->(@{$tpv->[$r]->{Position}}{qw(LatitudeDegrees LongitudeDegrees)}, @$mask[1 .. $#$mask])) {
	      $memo{ntps} -= 1;
	      $masked = 1;
	      last;
	    }
	  }

	  $masked or $tpv->[$s++] = $tpv->[$r];
	}

	splice @$tpv, $s;
	@$tpv and $trkv->[$v++] = $trkv->[$u];
      }

      splice @$trkv, $v;
      @$trkv and $lv->[$q++] = $lv->[$p];
    }

    splice @$lv, $q;
    @$lv and $av->[$j++] = $av->[$i];
  }

  splice @$av, $j;
}

if (@$av) {
  my $T = new FileHandle "> $to";

  defined $T || &dead($fit, "new FileHandle \"> $to\": $!");
  $T->print($start);

  my ($skip, $a);

  if ($tplimit > 0 && ($skip = $memo{ntps} / $tplimit) > 1) {
    foreach $a (@$av) {
      my $l;

      foreach $l (@{$a->{Lap}}) {
	my $t;

	foreach $t (@{$l->{Track}}) {
	  my $tpv = $t->{Trackpoint};
	  my ($j, @mv);

	  if ($tplimit_smart && defined $tpv->[0]->{AltitudeMeters}) {
	    for ($i = 1 ; $i < $#$tpv ;) {
	      my $updown;

	      for ($j = $i + 1 ; $j < @$tpv ; ++$j) {
		if (defined $tpv->[$j]->{AltitudeMeters}) {
		  if (($updown = $tpv->[$j]->{AltitudeMeters} - $tpv->[$i]->{AltitudeMeters})) {
		    last;
		  }
		}
	      }

	      if ($updown) {
		my $k;

		for ($k = $j + 1 ; $k < @$tpv ; ++$k) {
		  if (defined $tpv->[$k]->{AltitudeMeters}) {
		    if (($tpv->[$k]->{AltitudeMeters} - $tpv->[$j]->{AltitudeMeters}) / $updown < 0) {
		      last;
		    }
		  }
		}

		if ($k < @$tpv && $k - $i > $skip) {
		  push @mv, $k;
		}

		$i = $j;
	      }
	      else {
		last;
	      }
	    }
	  }

	  push @mv, $#$tpv + 1;

	  for ($i = $j = 1 ; @mv ;) {
	    my $m = shift @mv;
	    my $start = $i;
	    my $count;

	    for ($count = 0 ; $i < $m ; ++$j, ++$count) {
	      my $next = $start + int($count * $skip);
	      my ($k, $wsum, $wn, $csum, $cn);

	      for ($k = $i ; $k < $next && $k < $m ; ++$k) {
		my ($x, $tpx, $w);

		if (defined ($x = $tpv->[$k]->{Extensions}) &&
		    defined ($tpx = $x->{TPX}) &&
		    defined ($w = $tpx->{Watts})) {
		  $wsum += $w;
		  ++$wn;
		}

		if (defined $tpv->[$k]->{Cadence}) {
		  $csum += $tpv->[$k]->{Cadence};
		  ++$cn;
		}
	      }

	      $tpv->[$j] = $tpv->[$k - 1];
	      $tpv->[$j]->{Extensions}->{TPX}->{Watts} = $wsum / $wn if $wn > 0;
	      $tpv->[$j]->{Cadence} = $csum / $cn if $cn > 0;
	      $i = $k;
	    }
	  }

	  $j < @$tpv and splice @$tpv, $j;
	}
      }
    }
  }

  foreach $a (@$av) {
    &output($a, \%activity_def, $indent, $T);
  }

  $T->print($end);
  $T->close;
}

1;
__END__

=head1 NAME

Fit2tcx - converts a FIT file to a TCX file

=head1 SYNOPSIS

  fit2tcx -show_version=1
  fit2tcx [<options>] [<FIT activity file> [<TCX file>]]

=head1 DESCRIPTION

B<Fit2tcx> reads the contents of I<<FIT activity file>>,
converts them to correspoding TCX formats,
and write converted contents to I<<TCX file>>.

=for html The latest version is obtained via

=for html <blockquote>

=for html <!--#include virtual="/cgi-perl/showfile?/cycling/pub/fit2tcx-[0-9]*.tar.gz"-->.

=for html </blockquote>

It uses a Perl class

=for html <blockquote><a href="GarminFIT.shtml">

C<Garmin::FIT>

=for html </a></blockquote>

of version 0.10 or later.

=head2 Options

=over 4

=item C<-show_version=1>

shows the version string of this program,
and exits.

=item C<-verbose=1>

shows FIT file header and trailing CRC information on C<stdout>.

=item C<-tplimit=>I<<number>>

tries to limit the number of trackpoints to I<<number>>.

=item C<-must=>I<<list>>

specifies a comma separated list of TCX elements which must be included in trackpoints.

B<Fit2tcx> convert each C<record> 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.

Some map services seem to require a TCX file created with C<-must=Time,Position> option.

=item C<-tpexclude=>I<<list>>

specifies a comma separated list of TCX elements which should be excluded from C<Trackpoint> elements in I<<TCX file>>.

For instance,
with C<-tpexclude=AltitudeMeters> option,
B<fit2tcx> makes a TCX file including no altitude data in C<Trackpoint>s.

=item C<-include_creator=0>

specifies that a C<Creator> section should be excluded.

=item C<-lap=>I<<list>>

specifies a comma separated list of lap indices (0, 1, ...) which should be included in I<<TCX file>>.

Each element of I<<list>> must be of the form I<<index>> or I<<start>>C<->I<<end>>.

I<<index>> is treated as an abbreviation of I<<index>>C<->I<<index>>.

I<<start>>C<->I<<end>> implies that only laps with indices C<E<gt>=> I<<start>> and C<E<lt>=> I<<end>>,
should be included in I<<TCX file>>.

I<<start>> or I<<end>> may be one of an empty string, asterisc (C<*>), or the word C<ALL>,
which are treated as C<0> when used as I<<start>>, or C<65534> when used as I<<end>>.

For instance,
any of C<-lap=->, C<-lap=*>, or C<-lap=all> is treated as C<-lap=0-65534>.

=item C<-tpmask=>I<<list>>

specifies a colon or space separated list of I<<region>>s,
in which trackpoinsts must be excluded from I<<TCX file>>.

A I<<region>> must be a comma separated quadruple of the form I<<lat_sw>>C<,>I<<long_sw>>C<,>I<<lat_ne>>C<,>I<<long_ne>>.
I<<lat_*>> must be degrees of latitudes,
and I<<long_*>> must be degrees of longitudes.
Suffices I<_sw> and I<_ne> stand for "south west" and "north east", respectively.

Trackpoints in the "rectangle" (including borders) enclosed with paralles and meridians determined by the above latitudes and longitudes,
are not written to I<<TCX file>>.

=back

=head2 Per user configuration file C<.fit2tcx.pl>

B<Fit2tcx> evaluates the contents of the file C<.fit2tcx.pl> in your home directory if it exists,
before starting conversion.
So,
in the file,
you can set appropriate values to scalar variables of the same names of the above options with leading hyphens removed,
and will get the same effects as giving the command line options.

=head1 AUTHOR

Kiyokazu SUTO E<lt>suto@ks-and-ks.ne.jpE<gt>

=head1 DISCLAIMER etc.

This program is distributed with
ABSOLUTELY NO WARRANTY.

Anyone can use, modify, and re-distibute this program
without any restriction.

=head1 CHANGES

=head2 0.10 --E<gt> 0.11

=over 4

=item C<$tpmask>

accepts spaces as separators of I<<region>>s.

=item I<top level>

Fixed process hanging up with C<$tpmask> including tow or more I<<region>>s.

=back

=head2 0.09 --E<gt> 0.10

=over 4

=item C<&cb_record>

There was no check whether or not C<power> field exists in a C<record> message.

Thanks to report from S<Benjamin Wolak>.

=back

=head2 0.08 --E<gt> 0.09

=over 4

=item C<%activity_def>

order of sub-elemtens of C<Lap> was not conforming to C<http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd>.

Thanks again to report from S<David Garc>E<iacute>S<a Granda>.

=back

=head2 0.07 --E<gt> 0.08

=over 4

=item C<%activity_def>

Format of C<HeartRateBpm> in C<Trackpoint> was not conforming to C<http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd>.

Mandatory attribute C<Sport> of C<Activity> was missing.

New elements C<AverageHeartRateBpm> and C<MaximumHeartRateBpm> in C<Lap>.

C<TriggerMethod> in C<Lap> was mis-spelled as C<TrigerMethod>.

All thanks to report from S<David Garc>E<iacute>S<a Granda>.

=item C<$ENV{HOME}/.fit2tcx.pl>

loaded in a C<BEGIN> block,
based on report from S<David Garc>E<iacute>S<a Granda>.

=item C<Garmin::FIT>

loaded after C<$ENV{HOME}/.fit2tcx.pl>,
based on report from S<David Garc>E<iacute>S<a Granda>.

=back

=head2 0.06 --E<gt> 0.07

=over 4

=item C<&cmp_lon>

new subroutine to compare degrees of longitudes in right manner (hopefully).

=item C<&tpmask_rec>

use C<&cmp_lon>.

=item C<@tpmask>

ditto.

=back

=head2 0.05 --E<gt> 0.06

=over 4

=item I<top level>

improved accuracy when limitting the number of C<Trackpoint>s.

=back

=head2 0.04 --E<gt> 0.05

=over 4

=item C<$start>

C<xsi:schemaLocation> is made up in a consistent manner.

=back

=head2 0.03 --E<gt> 0.04

=over 4

=item C<$tpexclude>

new option.

=item C<$lap>

accepts an empty string, character C<*>, and word C<all>.

=back

=head2 0.02 --E<gt> 0.03

=over 4

=item C<%activity_def>

C<Track> elements should be considered arrays.

=item C<&cb_lap_or_session>

absorbed into C<&cb_lap>.

=back

=head2 0.01 --E<gt> 0.02

=over 4

=item C<%activity_def>

new member C<name>.

=item C<&output>

uses new member C<name> of hashes defining TCX elements.

=item I<top level>

C<Cadence>s in C<Trackpoint>s and C<Watts>'s in C<TPX>s were not re-calculated
when the option C<tplimit> was specified.

=back

=cut
