#!/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 . # # 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 <. 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 < $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__