#!/usr/local/bin/perl
# -----------------------------------------------------------------
#
# 'gpx2ovl.pl' - "GPX to Overlay"

# A script to convert .gpx tracks into polylines of the pol(n)ish
# map format, and compile them to a transparent "overlay map" for
# use with Garmin MapSource.
#
# IMPORTANT: This script is designed for use with v93c of cgpsmapper.
# Some parts of this script may NOT work with other versions, due to
# bugs and/or restricted functionality!
#
# Technical note: The approach to reading GPX format that I use here
# is not the most straightforward (easier: Perl::Geo), but it was my
# first attempt of reading read XML files. Thus, I deliberately used
# a rather "generic" approach here.
#
# Disclaimer: This code is based on another script with a similar
# purpose that I wrote ... it's still too bloated ;-)
#
# -----------------------------------------------------------------
# This program is free software; you can redistribute it and/or
# modify it under the terms of the version 2 of the GNU General
# Public License 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.
# -----------------------------------------------------------------
#
# Copyright (c) 2008 J. Hau <joerg.hau(at)dplanet.ch>.
#
# Revision History:
#   2008-02-14, JHa, first operational version
#   2008-02-18, JHa, lots of streamlining.
#   2008-02-29, JHa, patch for TDB file.
#   2008-03-02, JHa, added comments
#
# -----------------------------------------------------------------

use XML::DOM;       # XML reading
use File::Path;     # for deleting files (near end of script)

use strict;
use warnings;

$|=1;       	    # flush on (to show everything immediately)
my $DEBUG = 0;	    # set != 0 for debugging messages, or use -d on cmd line
my $EDIT = 0;		# set != 0 to edit the .mp file before compiling, or use -e on cmd line


# -----------------------------------------------------------------
# stuff that changes from edition to edition
# this could be passed on the cmd line or interactively, too ;-)
#
my $HeaderTitle="Tracks";       # map name displayed on GPSr, 80 chars maximum
my $HeaderLocation = "Tunisia";
my $HeaderYear = "2008";
my $FileName = "TunTracks";   # also used as MapSetName

# $MapSourceName is displayed "as such" in MapSource
my $MapSourceName = $HeaderTitle . " " . $HeaderLocation . " " . $HeaderYear; 

my $MapVersion = "803";       # must be between 000 and 999 ... I use a year-month code
my $ProductCode = 44;         # anything that is not yet in MapSource
my $Fid = 111;                # Family ID

# -----------------------------------------------------------------
# drawing styles
#
my $mode="POLYLINE";        # this is the default
my $type="0x0003";          # I frequently use 0x0002 (bold) and/or 0x0003 (thin)
my $typfile="mytrack";     # name for .TYP file, no extension


# -----------------------------------------------------------------
# Full path to map compiler.
# In my case, this is a symlink to cgpsmapper093c-static.
#
my $Mapper="./cgpsmapper";


# -----------------------------------------------------------------
# some global variables
#
my $gpxdoc;         # will hold the gpx data
my $gpxnodes;       # nodes in gpx doc


# -----------------------------------------------------------------
# Print usage mode
# -----------------------------------------------------------------
sub usage
{
print STDERR <<EOF
$0 - Read tracks in gpx format, converts them into
"pol(n)ish map format", and compiles them into a transparent
overlay map for use with Garmin MapSource.

Copyright (c) 2008 Joerg Hau <joerg.hau(at)dplanet.ch>.

This program is free software; you can redistribute it and-or
modify it under the terms of version 2 of the GNU General Public
License 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.

    Usage:  $0 [-d] [-e] trackfile.gpx

    Options:   -d  enable debug messages
               -e  edit the .mp file before compiling

EOF
}


# -----------------------------------------------------------------
# reads  coordinates in GPX format into global variables
# argument: filename to read (no wildcard)
# returns: 1 if OK, else dies
# -----------------------------------------------------------------
sub read_gpx {
  my $file = shift;
  my $parser = XML::DOM::Parser->new();

  $gpxdoc = $parser->parsefile($file);
  $gpxnodes = $gpxdoc->getElementsByTagName ('trk');
  print STDERR "'$file' has ". $gpxnodes->getLength ." Tracks.\n";
}


# -----------------------------------------------------------------
# generate file for generating ;-) the overview map
# arguments:
# - filename for the output file (with extension)
# - filename for the detailed map (with extension)
# Note: this uses a lot of global variables.
# -----------------------------------------------------------------
sub create_index {
  my ($outfile, $mapfile) = @_;

# Note: This map will be generated from an "empty" layer. If we would
# generate a "real" preview map, this would include _all_ POI etc ...
# which would slow down MapSource and overfill the display at zoom-out.
#
my $content="[Map]
FileName=$FileName
MapVersion=$MapVersion
ProductCode=$ProductCode
FID=$Fid
Levels=2
Level0=18
Level1=17
Zoom0=5
Zoom1=6
MapsourceName=$MapSourceName
MapSetName=$FileName
CDSetName=$HeaderTitle
Copy1=Created by Joerg_H
Copy2=This is a free map. Commercial distribution is NOT allowed!
[End-Map]

[Files]
img=$mapfile
[END-Files]
";

  print STDERR ("Writing '$outfile' ... ") if $DEBUG;
  open (OUT, ">", $outfile) || die "Can't open '$outfile' : $!";
  print OUT $content || die "Error in create_index(): $!";
  close (OUT);
  print STDERR ("done.\n") if $DEBUG;
}


# -----------------------------------------------------------------
# creates header for a single .mp file
# argument: map ID (a unique 8-digit number)
# note: OUT must be open!
# -----------------------------------------------------------------
sub print_header {
my $mapid=shift;

# The map will mainly consist of tracks. By default on my GPSmap 60CS,
# these are visible far too early for my purpose.
# To avoid this, we have to insert a layer with "something" that
# hides the POIs, until a pre-defined level is reached. Thus we need
# a total of 4 levels (explained from top to bottom):
#
# Level3=18 is an empty layer (required).
# Level2=19 defines a layer that is visible _until the next level_
#           (here, Level1) is reached. This layer will contain "something"
#           to hide the POI: Theoretically an empty polygon of the size of
#           the map is sufficient. However, cgpsmapper81 has a bug that
#           requires to give every single point his own "cover polygon".
# Level1=20 is present "just" to define the next level below the polygon(s).
#           Without this layer, the POI would only be visible once we reach
#           "their" level. - Data are identical to Level0.
# Level0=24 is the "data layer".
#
# If you want the data points to be visible "even further" in MapSource,
# you will probably have to introduce yet another layer (with the same
# information as in Level0/1)

my $hdr="[IMG ID]
ID=$mapid
Name=$HeaderTitle
LblCoding=9
CopyRight=Created by Joerg_H
Transparent=Y
Elevation=m
TreSize=1000
RgnLimit=1024
DrawPriority=1
Levels=4
Level0=24
Level1=20
Level2=19
Level3=18
[END]";

print OUT "$hdr\n\n" || die "Error in print_header(): $!";
}


# -----------------------------------------------------------------
# generates file for generating ;-) the typfile
# argument: filename for the output file (with extension)
# Note: this uses a lot of global variables.
# -----------------------------------------------------------------
sub create_typfile {
  my $outfile = shift;

  print STDERR ("Writing '$outfile' ... ") if $DEBUG;
  open (OUT, ">", $outfile) || die "Can't open '$outfile' : $!";

  print OUT <<EOF || die "Error in create_typfile(): $!";
[_ID]
ProductCode=$ProductCode
FID=$Fid
[End]

[_line]
Type=0x0002
LineWidth=3
BorderWidth=0
xpm="0 0 4 0"
"1 c #3366ff"
"2 c none"
"3 c #3366ff"
"4 c none"
string1=0x04,Track
[end]

[_line]
Type=0x0003
LineWidth=3
BorderWidth=0
xpm="0 0 4 0"
"1 c #00ff00"
"2 c none"
"3 c #00ff00"
"4 c none"
string1=0x04,Track
[end]

[_line]
Type=0x0004
LineWidth=3
BorderWidth=0
xpm="0 0 4 0"
"1 c #ff0000"
"2 c none"
"3 c #ff0000"
"4 c none"
string1=0x04,Track
[end]

[_drawOrder]
;Type=POLYGON_CODE(HEX),PRIORITY
Type=0x01,1
Type=0x02,1
Type=0x03,1
Type=0x04,1
Type=0x05,1
Type=0x06,1
Type=0x07,1
Type=0x08,3
Type=0x09,1
Type=0x0a,2
Type=0x0b,2
Type=0x0c,2
Type=0x0d,2
Type=0x0e,2
Type=0x13,2
Type=0x14,2
Type=0x15,2
Type=0x16,2
Type=0x17,3
Type=0x18,3
Type=0x19,3
Type=0x1a,4
Type=0x1e,2
Type=0x1f,2
Type=0x20,2
Type=0x28,1
Type=0x29,1
Type=0x32,1
Type=0x3b,1
Type=0x3c,8
Type=0x3d,8
Type=0x3e,8
Type=0x3f,8
Type=0x40,8
Type=0x41,8
Type=0x42,8
Type=0x43,8
Type=0x44,4
Type=0x45,2
Type=0x46,2
Type=0x47,2
Type=0x48,3
Type=0x49,4
Type=0x4c,5
Type=0x4d,5
Type=0x4e,5
Type=0x4f,5
Type=0x50,3
Type=0x51,6
Type=0x52,4
Type=0x53,5
[end]

EOF
  close (OUT);
  print STDERR ("done.\n") if $DEBUG;
}

# -----------------------------------------------------------------
# run cgpsmapper to compile the map and its preview map
# argument: full filename for map and preview map
# -----------------------------------------------------------------
sub compile_map {
  my ($mapfile, $previewfile) = @_;
  my $cmd;

  print STDERR ("Compiling map ...");  # show status

  # process the detailed map
  #
  $cmd = "$Mapper -l $mapfile > $FileName.log";
  print STDERR ("Executing '$cmd' ..") if $DEBUG;
  (system ($cmd) == 0) || die "Error in compile_map(): $!";
  print STDERR (" done.\n") if $DEBUG;

  # now process the overview map
  #
  $cmd = "$Mapper pv -l $previewfile >> $FileName.log";
  print STDERR ("Executing '$cmd' ..") if $DEBUG;
  (system ($cmd) == 0) || die "Error in compile_maps() at overview map: $!";
  unlink ($previewfile ) || die  "Can't delete $previewfile : $!";
  print STDERR (" done.\n") if $DEBUG;

  # process the TYP file
  #
  $cmd = "$Mapper typ $typfile.txt $typfile.typ >> $FileName.log";
  print STDERR ("Executing '$cmd' ..") if $DEBUG;
  (system ($cmd) == 0) || die "Error in compile_map(): $!";
  unlink ($typfile . ".txt") || die  "Can't delete $typfile.txt: $!";
  print STDERR (" done.\n");
}

# -----------------------------------------------------------------
# cgpsmapper 093c and 093d have a bug: The the FID of the .TDB file
# does not match the FID of the “other” files.
# This function patches the .TDB file and provides a correct FID.
# argument: full path to TDB file; FID
# -----------------------------------------------------------------
sub patch_tdb {
  my ($infile, $fid) = @_;
  my $outfile = "$$.tmp";
  my $buf;

  print STDERR ("Patching TDB file ... ");

  open (INF, "< $infile" ) or die "Error opening input '$infile': $!\n";
  open (OUTF, "> $outfile" ) or die "Error opening output '$outfile': $!\n";
  binmode INF;
  binmode OUTF;

  # read up to 64k from infile into buffer
  read (INF, $buf, 65535) or die "Problem reading: $!\n";

  # unpack buffer into string
  my $hex = unpack( "H*", $buf );

  # replace after position 10 for two bytes
  substr($hex, 10, 2) = sprintf("%02x", $fid);

  # pack buffer and write back into file
  print OUTF pack ("H*", $hex) or die "Problem writing: $!\n";

  close(INF);
  close(OUTF);
  rename ($outfile, $infile) || die  "Can't rename patched .TDB file: $!";
  print STDERR ("done.\n");
}

# -----------------------------------------------------------------
# generate REG file for Micro$**t Windows
# argument: none; uses global variables
# Yes I know, a number of things are hardcoded here ;-)
# -----------------------------------------------------------------
sub make_reg {
  my $fnam = $FileName . ".reg";
  my $path = "C:\\\\Garmin\\\\$FileName";

  print STDERR ("Generating registry file '$fnam' ... ") if $DEBUG;

  open (OUT, ">", $fnam) || die "Can't open '$fnam' : $!";

  print OUT "REGEDIT4\r\n\r\n";
  print OUT "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Garmin\\MapSource\\Families\\$FileName]\r\n";

  # Family ID as hex code. It works, but I'm not sure how this is supposed to be coded?
  my $code = sprintf("%02x",$Fid);
  print OUT "\"ID\"=hex:$code,00\r\n";
  print OUT "\"TYP\"=\"$path\\\\$typfile.typ\"\r\n\r\n";

  print OUT "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Garmin\\MapSource\\Families\\$FileName\\1]\r\n";
  print OUT "\"LOC\"=\"$path\\\\img\"\r\n";
  print OUT "\"BMAP\"=\"$path\\\\$FileName.img\"\r\n";
  print OUT "\"TDB\"=\"$path\\\\$FileName.tdb\"\r\n";

  close (OUT);
  print STDERR ("done.\n") if $DEBUG;
}


# -----------------------------------------------------------------
# move files into subdirectories
# argument: full name of the actual map file
# -----------------------------------------------------------------
sub pack_files {
  my $map = shift;
  my ($i, $fnam);

  print STDERR ("Moving files ... ");

  # if directory exists, remove it (!without asking!),
  # then create the new (empty) directrories
  #
  if ( -d $FileName ) {
    rmtree ($FileName) || die  "Can't rmtree '$FileName': $!";
    }
  mkdir ($FileName)  || die  "Can't mkdir '$FileName': $!";
  mkdir ($FileName . "/img/") || die  "Can't mkdir '$FileName/img/': $!";

  # move files
  #
  rename ($FileName. ".reg", $FileName ."/". $FileName. ".reg") || die  "Can't move .reg file: $!";
  rename ($FileName. ".img", $FileName ."/". $FileName. ".img") || die  "Can't move .img file: $!";
  rename ($FileName. ".TDB", $FileName ."/". $FileName. ".TDB") || die  "Can't move .TDB file: $!";
  rename ((uc($typfile)). ".TYP", $FileName ."/". $typfile. ".TYP") || die  "Can't move .TYP file: $!";
  rename ($map, $FileName . "/img/". $map) || die  "Can't move '$map': $!";

  # prepare to delete .mp file
  $map =~ s/img/mp/;
  (unlink ($map) || die  "Can't delete '$map': $!") unless $DEBUG;
  print STDERR ("done.\n");
}


# -----------------------------------------------------------------
# main program starts here
# -----------------------------------------------------------------
require Getopt::Std;
my %opt;                                   # to store the options
Getopt::Std::getopts('hde',\%opt);

# read command line options
#
if($opt{'h'} or @ARGV==0){                  # help
    usage();
    exit 0;
	}
if($opt{'d'}) {                             # debug messages on
    $DEBUG="1";
    }
if($opt{'e'}) {                             # editing on
    $EDIT="1";
    }

print STDERR "MapsourceName is \"$MapSourceName\", FileName is \"$FileName\", MapVersion is \"$MapVersion\".\n";

# read gpx file
#
my $file = shift;
read_gpx($file);

# generate unique map ID (= 8-digit number) from today's date :-)
#
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime(time);
my $id = sprintf "%4d%02d%02d", 1900+$year,$mon+1,$mday;
my $map = $id . ".mp";

# Now create the .mp file
#
print STDERR ("Writing '$map' ... ") if $DEBUG;
open (OUT, ">", $map) || die "Can't open '$map' : $!";
print_header ($id);                 # write header information

# fetch coordinates from gpx dataset and convert them into polylines
#
my ($lat, $lon);
my $n = $gpxnodes->getLength;         # number of tracks in file
for (my $i = 0; $i < $n; $i++)        # for all nodes (tracks)
    {
	my $node = $gpxnodes->item ($i);
	my $name = $node->getElementsByTagName('name')->item(0)->getFirstChild->getNodeValue;
	my $done = 0;                   # flag to indicate that first output was done
	print OUT "\n[$mode]\n";
    print OUT "Type=$type\nLabel=". $name. "\nEndLevel=1\nData0=";
  	foreach my $ref ($node->getElementsByTagName('trkpt')){
		$lat = $ref->getAttribute('lat');
		$lon = $ref->getAttribute('lon');
		print OUT "," if (($lat) && ($done));
		print OUT "(" . $lat . "," . $lon . ")";
		$done = 1;
		}
    print OUT "\n[END]\n";
    }

close (OUT);
print STDERR (" done: $n tracks.\n") if $DEBUG;

$gpxdoc->dispose;  # clean up gpx

# if editing was desired, pause here and allow for editing of the .mp file
#
if ($EDIT) {
	print STDERR ("Edit the file '$map', then press any key to continue ...");
	<>;    #     wait for keypress
	}


# build the "overview" file, then compile the two maps, and clean up:
#
my $fnam = $FileName . ".txt";
create_index ($fnam,  $id . ".img");
create_typfile ($typfile . ".txt");
compile_map ($map, $fnam);
make_reg();
patch_tdb $FileName. ".TDB", $Fid;
pack_files($id . ".img");

print STDERR ("Finished.\n");

1;

__END__
