#!/usr/bin/perl

# Copyright (C) 2010, 2011, 2012, 2014 Thorsten Kukuk
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# in Version 2 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA  02110-1301, USA.

=head1 NAME

geo-fixgpx - Modify GPX files for Garmin GPS devices

=head1 SYNOPSIS

geo-fixgpx [options] [<gpx-file> ...]

=head1 DESCRIPTION

geo-fixgpx modifies the GPX files from geocaching.com. It can
adjust the coordinates of caches to the final ones (especially
important for Mysteries) and remove parking waypoints (if you go
geocaching by bike, you don't need them). If waypoints of stages
have an empty comment field, the description field is copied, so
that you can see on Oregons and GPSmap 62s which Stage this is.
With a heuristic geo-fixgpx tries to add the GC Code and the
Cache name, if that entry was already processed before.

HTML code in the long description field will be replaced with
UTF-8 characters.
Attributes will be added to the long description of a cache.

=head1 OPTIONS

  -c|--coords <file>	Text file with corrected coordinates
  -d|--date             Add current date as suffix for modified gpx files
  -i|--ignore <file>	List of waypoints which should not be modified
                        or removed
  --gcdate              Add date when geocache was hidden to listing
  --gcvote              Add GCVote to listing
  --gcprefix <prefix>   Add prefix to all cache names 
  -k|--keep <file>      List of waypoints which should be modified but
                        not removed
  -r|--remove <file>    List of waypoints which should be removed
  -p|--remove-parking-area
			Remove parking area waypoints
  -m|--remove-mysteries Remove mystery caches which have no modified
                        coordinates and are no bonus
  -M|--mark <file>      List of caches which should be marked special
  --remove-archived     Remove archived caches if not in -c or -k list
  --remove-disabled     Remove disabled caches if not in -c or -k list
  -w|--warn-missing     Warn about Geocaches listed in coords file
                        but not available in a GPX file. Only
                        useable with -c|--coords
  -q|--quiet            Be quiet and only print error messages
  --html		Convert long descriptions to HTML code
  --dump-config         Write arguments as default config and exit
  --version             Print version number and exit
  --man                 Display manual page
  --usage               Display usage
  -h|-?|--help          Help

=head1 INPUT

Format of the coordinates file:

  GC-Code, Latitude Longitude[, Note][# comment]
  [...]

Format of the ignore and keep file:

   GC-Code
   [...]

Empty lines and lines starting with # are ignored.


=head1 COPYRIGHT

Copyright (c) 2010 by Thorsten Kukuk.  All rights reserved.

This package is free software; you can redistribute it and/or modify
it under the GPL version <http://gnu.org/licenses/gpl2.html>.
There is NO WARRANTY, to the extent permitted by law.

=cut

use strict;
use warnings;
use XML::Twig;
use Pod::Usage;
use GEO::Coords;
use GEO::GCVote;
use Config::IniFiles;

my $Version = '(geo-tools) 1.23';

#
# process command line arguments
#
use Getopt::Long;
my $help = 0;
my $man = 0;
my $version = 0;
my $dump_config = 0;
my $homedir = '';

if ($ENV{HOME}) {
  $homedir = $ENV{HOME};
} elsif ($ENV{HOMEDRIVE} && $ENV{HOMEPATH}) {
  $homedir = $ENV{HOMEDRIVE}."/".$ENV{HOMEPATH};
}
my $cfgname = $homedir."/.geo-tools.cfg";

if (!-r $cfgname) {
  my $cfg = new Config::IniFiles(-default => "Global",
				 -nocase => 1);
  $cfg->AddSection("geo-fixgpx");
  $cfg->newval("geo-fixgpx", "convert2utf8", 1);
  $cfg->WriteConfig($cfgname);
}

my $cfg = new Config::IniFiles(-file => $cfgname,
			       -default => "Global");
if (!$cfg->SectionExists("geo-fixgpx")) {
  $cfg->AddSection("geo-fixgpx");
  $cfg->newval("geo-fixgpx", "convert2utf8", 1);
  $cfg->WriteConfig($cfgname);
}
my $c2utf8 = $cfg->val("geo-fixgpx", "convert2utf8", 1);
my $cleanhtml = $cfg->val("geo-fixgpx", "cleanup-htmltags", 1);
my $rmpark = $cfg->val("geo-fixgpx", "remove-parking-area", 0);
my $c2html = $cfg->val("geo-fixgpx", "convert2html", 0);
my $rmmystery = $cfg->val("geo-fixgpx", "remove-mysteries", 0);
my $rmarchived = $cfg->val("geo-fixgpx", "remove-archived", 0);
my $rmdisabled = $cfg->val("geo-fixgpx", "remove-disabled", 0);
my $warn_missing = $cfg->val("geo-fixgpx", "warn-missing", 0);
my $coords = $cfg->val("geo-fixgpx", "coordinates");
my $ignore = $cfg->val("geo-fixgpx", "ignorelist");
my $keep = $cfg->val("geo-fixgpx", "keeplist");
my $remove = $cfg->val("geo-fixgpx", "removelist");
my $mark = $cfg->val("geo-fixgpx", "marklist");
my $date = $cfg->val("geo-fixgpx", "date", 0);
my $quiet = $cfg->val("geo-fixgpx", "quiet", 0);
my $gcvote = $cfg->val("geo-fixgpx", "gcvote", 0);
my $gcdate = $cfg->val("geo-fixgpx", "gcdate", 0);
my $gcprefix = $cfg->val("geo-fixgpx", "gcprefix");

GetOptions('c|coords=s' => \$coords,
	   'd:1' => \$date,
	   'date!' => \$date,
	   'i|ignore=s' => \$ignore,
	   'k|keep=s' => \$keep,
	   'r|remove=s' => \$remove,
	   'M|mark=s' => \$mark,
	   'p:1' => \$rmpark,
	   'remove-parking-area!' => \$rmpark,
	   'm:1' => \$rmmystery,
	   'remove-mysteries!' => \$rmmystery,
	   'remove-archived!' => \$rmarchived,
	   'remove-disabled!' => \$rmdisabled,
	   'html!' => \$c2html,
	   'w:1' => \$warn_missing,
	   'warn-missing!' => \$warn_missing,
	   'q:1' => \$quiet,
	   'quiet!' => \$quiet,
	   'gcvote!' => \$gcvote,
	   'gcdate!' => \$gcdate,
	   'gcprefix=s' => \$gcprefix,
	   'dump-config' => \$dump_config,
	   'man' => \$man,
	   'version' => \$version,
	   'help|h|?' => \$help) or pod2usage(2);
pod2usage(0) if $help;
pod2usage(-exitstatus => 0, -verbose => 2) if $man;

if ($version) {
  print "geo-fixgpx $Version\n";
  exit 0;
}

if ($dump_config) {
  WriteConfig();
  exit 0;
}

if ($date) {
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime(time);
  $date = sprintf "%4d%02d%02d%02d%02d",
    $year+1900,$mon+1,$mday,$hour,$min;
}

my @InputFiles;
if ($#ARGV >= 0) {
   @InputFiles = (@ARGV);
}
else {
  print "Searching for GPX files...\n" unless $quiet;
  GetGPXFiles();
  if ($#InputFiles == -1) {
    print "No input files found\n";
    exit(1);
  }
}

my %KeepWaypoint;
my %IgnoreCaches;
my %RemoveCaches;
my %CorrectedCaches;
my %MarkCaches;
my %KnownCaches;
my $Modified;

my %html2utf8 = ('&amp;auml;' => "ä",
		 '&amp;Auml;' => "Ä",
		 '&amp;ouml;' => "ö",
		 '&amp;Ouml;' => "Ö",
		 '&amp;uuml;' => "ü",
		 '&amp;Uuml;' => "Ü",
		 '&amp;szlig;' => "ß",
		 '&amp;bdquo;' => "„",
		 '&amp;ldquo;' => "“",
		 '&amp;rdquo;' => "”",
		 '&amp;ndash;' => "–",
#		 '&amp;nbsp;' => " "
		);
my $regex = qr/${ \(join'|', map quotemeta, keys %html2utf8)}/;

print "Getting ignore list...\n" unless $quiet;
LoadCacheList ($ignore, \%IgnoreCaches);
print "Getting corrections list...\n" unless $quiet;
GetCorrectedCaches ();
print "Getting waypoints to keep...\n" unless $quiet;
LoadCacheList ($keep, \%KeepWaypoint);
print "Getting waypoints to remove...\n" unless $quiet;
LoadCacheList ($remove, \%RemoveCaches);
print "Getting caches to mark...\n" unless $quiet;
LoadCacheList ($mark, \%MarkCaches);

foreach my $InputFileName (@InputFiles) {
  my $Parser =
    new XML::Twig(twig_handlers=>{'gpx/wpt' => \&GetWaypoint},
		  keep_encoding => 1);
  my $OutputFileName = $InputFileName;
  if ($date) {
    $OutputFileName =~ s/.gpx$/-$date.gpx/;
  } else {
    $OutputFileName =~ s/.gpx$/-mod.gpx/;
  }
  $Modified = 0;

  print "Processing input GPX file $InputFileName:\n" unless $quiet;
  $Parser->parsefile($InputFileName);

  if ($Modified) {
    print "$InputFileName modified, creating $OutputFileName\n" unless $quiet;
    $Parser->print_to_file($OutputFileName, pretty_print => 'indented');
  }
}

if ($warn_missing) {
  my ($k, $v);
  my $first = 1;

  while (($k,$v) = each %CorrectedCaches) {
    if ($v->{Found} == 0) {
      if ($first) {
	$first = 0;
	print "Missing entries:\n";
      }
      print "$k, ";
      print GEO::Coords::sprintf_mindec($v->{Latitude}, $v->{Longitude});
      print ", $v->{Note}" if ($v->{Note});
      print " # $v->{Comment}" if ($v->{Comment});
      print "\n";
    }
  }
}

print "\nDone!\n" unless $quiet;
exit;

sub WriteConfig
{
  $cfg->newval("geo-fixgpx", "convert2utf8", $c2utf8);
  $cfg->newval("geo-fixgpx", "cleanup-htmltags", $cleanhtml);
  $cfg->newval("geo-fixgpx", "convert2html", $c2html);
  $cfg->newval("geo-fixgpx", "date", $date);
  $cfg->newval("geo-fixgpx", "quiet", $quiet);
  $cfg->newval("geo-fixgpx", "remove-parking-area", $rmpark);
  $cfg->newval("geo-fixgpx", "remove-mysteries", $rmmystery);
  $cfg->newval("geo-fixgpx", "remove-archived", $rmarchived);
  $cfg->newval("geo-fixgpx", "remove-disabled", $rmdisabled);
  $cfg->newval("geo-fixgpx", "warn-missing", $warn_missing);
  $cfg->newval("geo-fixgpx", "coordinates", $coords);
  $cfg->newval("geo-fixgpx", "ignorelist", $ignore);
  $cfg->newval("geo-fixgpx", "keeplist", $keep);
  $cfg->newval("geo-fixgpx", "removelist", $keep);
  $cfg->newval("geo-fixgpx", "marklist", $mark);
  $cfg->newval("geo-fixgpx", "gcvote", $gcvote);
  $cfg->newval("geo-fixgpx", "gcdate", $gcdate);
  $cfg->newval("geo-fixgpx", "gcprefix", $gcprefix);
  $cfg->WriteConfig($cfgname);
}

sub GetGPXFiles
{
  opendir DIR, '.';
  foreach my $FileName (sort { "$b" cmp "$a" } readdir DIR) {
    push @InputFiles, ($FileName) if ($FileName =~ m/\.gpx$/i);
  }
  closedir DIR;
}

sub GetWaypoint
{
  my ($t, $wpt) = @_;
  my $ID = $wpt->first_child_text('name');
  my $type = $wpt->first_child_text('type');
  my $symbol = $wpt->first_child_text('sym');
  my $hidden = $wpt->first_child_text('time');
  my $cache = $wpt->first_child('groundspeak:cache');
  my $name = $cache->first_child('groundspeak:name') if $cache;
  print "Parsing ID: $ID       \r" unless $quiet;

  #
  # Fix the EarthCache in PQ
  #
  if ($type eq 'Geocache|EarthCache') {
    my $typechild = $wpt->first_child('type');
    $typechild->set_text('Geocache|Earthcache');
    $typechild = $cache->first_child('groundspeak:type');
    $typechild->set_text('Earthcache');
    $Modified = 1;
  }

  #
  # Fix the Mystery Cache in PQ
  #
  if ($type eq 'Geocache|Mystery Cache') {
    my $typechild = $wpt->first_child('type');
    $typechild->set_text('Geocache|Unknown Cache');
    $typechild = $cache->first_child('groundspeak:type');
    $typechild->set_text('Unknown Cache');
    $Modified = 1;
  }

  #
  # Create a list of known Geocaches and Description,
  # which we could use later for waypoints.
  #
  $KnownCaches{$ID} = $name->text if $cache;


  if ($IgnoreCaches{$ID}) {
    print "$ID: Ignoring...          \r" unless $quiet;
    return;
  }

  if ($RemoveCaches{$ID}) {
    print "$ID: On remove list, deleted\n" unless $quiet;
    $Modified = 1;
    $_->delete;
    return;
  }

  # Parking Area waypoints are deleted if requested.
  if ($rmpark && $type eq 'Waypoint|Parking Area' && !$KeepWaypoint{$ID}) {
    print "$ID: Delete Waypoint\n" unless $quiet;
    $Modified = 1;
    $_->delete;
    return;
  }

  # only proceed if this entry is a Waypoint for a geocache
  if ($type =~ m/^Waypoint\|/ ) {
    # If cmt field is empty, write desc into cmt field
    my $cmt = $wpt->first_child('cmt');
    if ($cmt->text eq '') {
      $Modified = 1;
      $cmt->set_text ($wpt->first_child_text('desc'));
    }

    # Look if we know the cache and add the GC ID.
    my $cacheID = $ID;
    $cacheID =~ s/^../GC/;
    # If Cache was removed, remove corresponding Waypoints, too
    if ($RemoveCaches{$cacheID}) {
      print "$ID: Delete Waypoint (part of $cacheID)\n" unless $quiet;
      $Modified = 1;
      $_->delete;
      return;
    }
    if ($KnownCaches{$cacheID}) {
      $Modified = 1;
      my $new_cmt = "$cacheID: $KnownCaches{$cacheID}\n".$cmt->text;
      $cmt->set_text ($new_cmt);
    }
  }

  # Modify coordinates if we know better one.
  if (defined $CorrectedCaches{$ID}) {
    # don't modify a modifed entry again.
    return if ($name->text =~ m/\(S\)/);

    print "$ID: Adjust coordinates\n" unless $quiet;
    $Modified = 1;
    $wpt->{'att'}->{'lat'} = $CorrectedCaches{$ID}->{Latitude};
    $wpt->{'att'}->{'lon'} = $CorrectedCaches{$ID}->{Longitude};
    $CorrectedCaches{$ID}->{Found} = 1;

    if ($cache) {
      $name->set_text ("(S)" . $name->text);
    } else {
      # XXX Missing if only waypoint, no cache
    }
  } elsif ($rmmystery && ($type eq 'Geocache|Unknown Cache' ||
			  $type eq 'Geocache|Mystery Cache') &&
	   !$KeepWaypoint{$ID}) {
    # Don't delete if bonus, challanges, solved or chirp.
    if ($name->text !~ m/bonus/i &&
	$name->text !~ m/challenge/i &&
	$name->text !~ m/\(S\)/ &&
	$name->text !~ m/chirp/i ) {
      print "$ID: Delete unsolved Mystery\n" unless $quiet;
      # Add to list of Caches for removal to delete corresponding
      # Waypoints, too
      $RemoveCaches{$ID} = 1;
      $Modified = 1;
      $_->delete;
      return;
    }
  }

  if (defined $gcprefix && $gcprefix && $cache) {
    print "$ID: Add prefix\n" unless $quiet;
    $Modified = 1;
    $name->set_text ($gcprefix. $name->text);
  }
  
  #
  # if this is no geocache, but only a waypoint, stop here
  #
  if (!$cache) {
    return;
  }

  # check vor GCVote
  my ($gcvoteMedian, $gcvoteAvg);
  if ($gcvote) {
    my $gcvote = new GEO::GCVote ();
    ($gcvoteMedian, $gcvoteAvg) = $gcvote->gcvote($ID);
  }

  # some often used descriptions.
  my $short_desc = $cache->first_child('groundspeak:short_description');
  my $ShortDescription = $short_desc->text;
  my $long_desc = $cache->first_child('groundspeak:long_description');
  my $LongDescription = $long_desc->text;

  if ($c2utf8) {
    #
    # Replace some HTML code with UTF-8 characters
    #
    $name->set_text(convert2utf($name->text));
    $ShortDescription =~ s/($regex)/$html2utf8{$1}/g;
    $LongDescription =~ s/($regex)/$html2utf8{$1}/g;
  }

  # if short or long description is not HTML, convert it to html
  if ($c2html) {
    if ($short_desc->{'att'}->{'html'} eq "False") {
      $short_desc->{'att'}->{'html'} = "True";
      # Replace "\n" with "<br>\n".
      $ShortDescription =~ s/[\r]*\n[\r]*/&lt;br \/&gt;\n/g;
    }
    if ($long_desc->{'att'}->{'html'} eq "False") {
      $long_desc->{'att'}->{'html'} = "True";
      # Replace "\n" with "<br>\n".
      $LongDescription =~ s/[\r]*\n[\r]*/&lt;br \/&gt;\n/g;
    }
  }

  # Remove and modify HTML tags with which the 2s has problems.
  if ($cleanhtml) {
    $ShortDescription = cleanup_htmltags($ShortDescription);
    $LongDescription = cleanup_htmltags($LongDescription);
  }

  # Mark cache if it is already archived, currently disabled or
  # already found.
  if ($cache->{'att'}->{'archived'} eq "True") {
    $Modified = 1;
    if ($rmarchived && !$CorrectedCaches{$ID} && !$KeepWaypoint{$ID}) {
      $_->delete;
      return;
    } else {
      $name->set_text ("(A)" . $name->text);
    }
  } elsif ($cache->{'att'}->{'available'} eq "False") {
    $Modified = 1;
    if ($rmdisabled && !$CorrectedCaches{$ID} && !$KeepWaypoint{$ID}) {
      $_->delete;
      return;
    } else {
      $name->set_text ("(D)" . $name->text);
    }
  }

  if ($symbol =~ /Geocache Found/i) {
    $Modified = 1;
    $name->set_text ("(F)" . $name->text);
  }
  if ($MarkCaches{$ID} || ($gcvote && $gcvoteMedian && $gcvoteMedian > 3.7)) {
    $Modified = 1;
    $name->set_text ("*" . $name->text);
  }

  #
  # If there are notes with the new coordinates, add them to the desc.
  #
  if (defined $CorrectedCaches{$ID}) {
    if ($CorrectedCaches{$ID}->{Note}) {
      my $Note;
      if ($short_desc->{'att'}->{'html'} eq "True" || $c2html) {
	$Note = "&lt;br /&gt;&lt;u&gt;Note:&lt;/u&gt;&lt;br /&gt;\n";
	$Note .= $CorrectedCaches{$ID}->{Note};
	$Note .= "&lt;hr /&gt;\n\n";
      } else {
	$Note = "Note:\n";
	$Note.= $CorrectedCaches{$ID}->{Note};
	$Note .= "\n\n";
      }
      $ShortDescription = $Note.$ShortDescription
    }
  }

  # Write Hide date and GCVote voting at the beginning
  if ($gcdate || ($gcvote && $gcvoteAvg)) {
    my $Note="";

    if ($gcdate) {
      $hidden = substr ($hidden, 0, 10);
      if ($short_desc->{'att'}->{'html'} eq "True" || $c2html) {
	$Note .= "&lt;br /&gt;&lt;u&gt;Versteckt:&lt;/u&gt; $hidden&lt;br /&gt;\n";
      } else {
	$Note .= "Versteckt: $hidden\n";
      }
    }
    if ($gcvote && $gcvoteAvg) {
      my $vote = sprintf ("%1.1f", $gcvoteAvg);
      if ($short_desc->{'att'}->{'html'} eq "True" || $c2html) {
	$Note .= "&lt;br /&gt;&lt;u&gt;GCVote:&lt;/u&gt; $vote&lt;br /&gt;\n";
      } else {
	$Note .= "GCVote: $vote\n";
      }
    }
    if ($short_desc->{'att'}->{'html'} eq "True" || $c2html) {
      $Note .= "&lt;hr /&gt;\n\n";
    } else {
      $Note .= "\n";
    }
    $ShortDescription = $Note.$ShortDescription
  }

  #
  # If there are attributes, get them and add them to the long description
  #
  my $attrs = $cache->first_child('groundspeak:attributes');
  if ($attrs) {
    my $i = 0;
    my $attrtext = "";
    foreach my $attr ($attrs->children('groundspeak:attribute')) {
      if ($i++ != 0) {
        $attrtext .= ", ";
      }
      if ($attr->{'att'}->{'inc'}) {
        $attrtext .= $attr->text;
      } else {
        $attrtext .= "!".$attr->text;
      }
    }
    if ($i != 0) {
      if ($long_desc->{'att'}->{'html'} eq "True" || $c2html) {
	$LongDescription .= "\n&lt;br /&gt;&lt;u&gt;Attribute:&lt;/u&gt;&lt;br /&gt;";
	$LongDescription .= $attrtext;
	$LongDescription .= "&lt;br /&gt;\n";
      } else {
	$LongDescription .= "\nAttribute:\n";
	$LongDescription .= $attrtext;
	$LongDescription .= "\n";
      }
    }
  }

  # if we modified the description, write them back
  if ($ShortDescription ne $short_desc->text) {
    $Modified = 1;
    $short_desc->set_text ($ShortDescription);
  }
  if ($LongDescription ne $long_desc->text) {
    $Modified = 1;
    $long_desc->set_text ($LongDescription);
  }

  # Modify the logs, too
  my $logs = $cache->first_child('groundspeak:logs');
  if ($logs) {
    foreach my $log ($logs->children('groundspeak:log')) {
      my $txt = $log->first_child('groundspeak:text');

      if ($txt->{'att'}->{'encoded'} eq "False") {
	my $text = $txt->text;

	$text =~ s/($regex)/$html2utf8{$1}/g if ($c2utf8);
	$text = cleanup_htmltags ($text) if ($cleanhtml);

	if ($txt->text ne $text) {
	  $Modified = 1;
	  $txt->set_text($text);
	}
      }
    }
  }
}

sub convert2utf
{
  my $str = $_[0];

  $str =~ s/($regex)/$html2utf8{$1}/g;

  return $str;
}

sub cleanup_htmltags
{
  my $str = $_[0];

  # Remove all font tags:
  $str =~ s|&lt;font.*?&gt;||gs;
  $str =~ s|&lt;/font\s*&gt;||gs;

  # Remove all strong tags:
  $str =~ s|&lt;strong.*?&gt;||gs;
  $str =~ s|&lt;/strong\s*&gt;||gs;

  # Remove all big tags:
  $str =~ s|&lt;big.*?&gt;||gs;
  $str =~ s|&lt;/big\s*&gt;||gs;

  # Replace smileys with text
  $str =~ s/&lt;img\s+src=\s*'http:\/\/www.geocaching.com\/images\/icons\/icon_smile_wink.gif'.*?\/&gt;/;)/gs;
  $str =~ s/&lt;img\s+src=\s*'http:\/\/www.geocaching.com\/images\/icons\/icon_smile.gif'.*?\/&gt;/:)/gs;
  $str =~ s/&lt;img\s+src=\s*'http:\/\/www.geocaching.com\/images\/icons\/icon_smile_big.gif'.*?\/&gt;/:D/gs;
  # For new logs after 9.11.2010
  $str =~ s/&lt;img\s+src=\s*"\/images\/icons\/icon_smile_wink.gif".*?\/&gt;/;)/gs;
  $str =~ s/&lt;img\s+src=\s*"\/images\/icons\/icon_smile.gif".*?\/&gt;/:)/gs;
  $str =~ s/&lt;img\s+src=\s*"\/images\/icons\/icon_smile_big.gif".*?\/&gt;/:D/gs;

  # Replace list type (<ul>/<li></li>/</ul>)
  $str =~ s/&lt;ul.*&gt;/&lt;br \/&gt;\n/g;
  $str =~ s/&lt;li&gt;/ * /g;
  $str =~ s|&lt;/li&gt;|&lt;br \/&gt;\n|g;
  $str =~ s|&lt;/ul&gt;|&lt;br \/&gt;\n|g;

  # Replace img links
  $str =~ s/&lt;img\s+src=.*?&gt;/[Picture]/gsi;

  return $str;
}

sub LoadCacheList
{
  my $filename = $_[0];
  my $Caches = $_[1];

  return if (!$filename || $filename eq '');

  if (-r $filename) {
    open(FILE, "<", $filename);
    my @Lines = readline(*FILE);
    foreach my $Line (@Lines) {
      if ($Line =~ /^\s*(GC\w{3,})/) {
	$$Caches{$1} = 1;
      }
    }
    close FILE;
  } else {
    print STDERR "ERROR: $filename does not exist\n";
    exit 1;
  }
}

sub encode_text
{
  my $str = $_[0];

  return $str unless defined $str;

  $str =~ s/&/&amp;/g;
  $str =~ s/</&lt;/g;
  $str =~ s/>/&gt;/g;

  return $str;
}

sub GetCorrectedCaches
{
  return if (!$coords || $coords eq '');

  if (-r $coords) {
    open(CORRECTFILE, "<", $coords);
    my @Lines = readline(*CORRECTFILE);
    foreach my $Line (@Lines) {
      my @Fields = SplitInputLine($Line);
      if ($#Fields >= 1) {
	my ($lat, $lon) = GEO::Coords::parse2($Fields[1]);
	if ($lat && $lon) {
	  $CorrectedCaches{$Fields[0]} =
	    {
	     Latitude => $lat,
	     Longitude => $lon,
	     Note => encode_text ($Fields[2]),
	     Comment => encode_text ($Fields[3]),
	     Found => 0
	    }
	  } else {
	    print STDERR "ERROR: Cannot parse coordinates for $Fields[0]\n";
	    eixt (1);
	  }
	}
      elsif ($#Fields == 0) {
	print STDERR "ERROR: CorrectedCaches $Fields[0]\n";
	exit(1);
      }
    }
    close CORRECTFILE;
  } else {
    print STDERR "ERROR: $coords does not exist\n";
    exit 1;
  }
}

sub SplitInputLine
{
  my $Line = $_[0];
  my @Fields = ();
  my $comment;
  chomp($Line);

  if ($Line =~ /^\s*\#/) {
    return @Fields;
  }

  return @Fields if (length($Line) == 0);

  if ($Line =~ /.*\#/) {
    $comment = $Line;
    $comment =~ s/.*\#\s*//;
    $Line =~ s/\s*\#.*//;
  }

  if ($Line =~ /\s*(\w+)\s*,\s*([^,]+)\s*,\s*(.*)/) {
    $Fields[0] = $1;
    $Fields[1] = $2;
    $Fields[2] = $3;
  } elsif ($Line =~ /\s*(\w+)\s*,\s*([^,]+)\s*/) {
    $Fields[0] = $1;
    $Fields[1] = $2;
  } else {
    print "ERROR: Cannot parse: $Line\n";
    #exit 1;
  }

  $Fields[3] = $comment;

  return @Fields;
}
