randomfox (randomfox) wrote,

Perl script to merge/convert EasyGPS binary .loc files and Geocaching .loc files

This script reads waypoints from any number of EasyGPS binary .loc and Geocaching .loc files on the command line (wildcards supported in DOS/Windows), combines all those waypoints, and sends the result in Geocaching .loc format to standard output.

This is useful if you are using an older version of EasyGPS to edit waypoint files but wish to convert and merge waypoint files into the Geocaching .loc format for use in newer versions of EasyGPS.

Note: gcmerge.pl produced incorrect UTF-8 output until I installed XML::SAX. I do not know why it was broken before but this is worth a try if EasyGPS complains about invalid characters in the Geocaching .loc format output from this script.

Update 1/25/2010: Added support for reading GPX files.

Update 2/25/2010: Slurp in entire waypoint files and implement buffering ourselves for speed.


#!perl -w
#
# Usage: gcmerge.pl fname.loc [fname.loc ...]
#
# This script takes any number of EasyGPS binary .loc, Geocaching .loc, and
# GPX .gpx files on the command line (wildcards supported in DOS/Windows),
# combines all the waypoints, and sends the result in Geocaching .loc
# format to standard output. The script automatically detects the format of
# each waypoint file and parses it accordingly.
#
# This is useful if you are using an older version of EasyGPS to edit
# waypoint files but wish to convert and merge waypoint files to the newer
# Geocaching .loc format since the older EasyGPS binary format is not well-
# supported by new software.
#
# Author: Po Shan Cheah
# Email: morton@mortonfox.com
# Last updated: February 25, 2010

use strict;
use FileHandle;
use Data::Dumper;
use XML::Simple;
use File::DosGlob;


BEGIN {
    my $wayptbuf;
    my $wayptpos;

    # Open and read in the entire waypoint file.
    sub waypt_open {
	my $fname = shift;

	my $wayptfh = new FileHandle $fname, "r";
	defined $wayptfh or die "Can't open $fname for reading: $!\n";

	binmode $wayptfh;
	sysread($wayptfh, $wayptbuf, -s $wayptfh);
	close $wayptfh;

	$wayptpos = 0;
    }

    # Get the entire waypoint file buffer. For XML parsing.
    sub waypt_getbuf {
	$wayptbuf;
    }

    # Get one character from the buffer. Updates buffer position after
    # that. Returns undef if at end of buffer.
    sub waypt_getc {
	$wayptpos >= length $wayptbuf and return;
	substr($wayptbuf, $wayptpos++, 1);
    }

    # Get a substring from the buffer. Updates buffer position after that.
    sub waypt_read {
	my $len = shift;
	$wayptpos >= length $wayptbuf and return "";
	my $s = substr($wayptbuf, $wayptpos, $len);
	$wayptpos += length $s;
	$s;
    }

    # Read a null-terminated string from the buffer.
    sub waypt_read_cstr {
	my $s = unpack("Z*", substr($wayptbuf, $wayptpos));
	$wayptpos += 1 + length $s;
	$s;
    }

    # Returns true if at end of buffer.
    sub waypt_eof {
	$wayptpos >= length $wayptbuf;
    }
}

# Read the header of an EasyGPS .loc file.
# Returns undef if not an EasyGPS file.
sub easygps_read_header {
    my $hdrsig = "TerraByte Location File";

    my $hdr = waypt_read(52);

    if (length $hdr < 52 or
	substr($hdr, 0, length $hdrsig) ne $hdrsig or
	substr($hdr, 51, 1) ne 'W') {

	# warn "Not an EasyGPS file\n";
	return;
    }

    1;
}

# Read a Pascal-style (length followed by chars) string from a file.
sub read_pstr {
    my $str;
    unless (defined($str = waypt_getc())) {
	warn "Premature EOF";
	return;
    }

    my $len = ord($str);
    $str = waypt_read($len);
    if (length $str < $len) {
	warn "Premature EOF";
	return;
    }

    $str;
}

# Read a C-style (null-terminated) string from a file.
sub read_cstr {
    waypt_read_cstr();
}

# Read a little-endian double from a file.
sub read_dbl {
    my $str = waypt_read(8);
    if (length $str < 8) {
	warn "Premature EOF";
	return;
    }

    scalar(unpack("d<", $str));
}

# Read waypoints from an EasyGPS .loc file.
# Code was adapted from easygps.c in GPSBabel. See http://www.gpsbabel.org/
sub easygps_read_waypt {
    my $waypt = {};

    while (1) {
	my $str;
	my $bytesread;

	unless (defined($str = waypt_getc())) {
	    warn "Premature EOF while reading waypoint";
	    return;
	}

	my $tag = ord $str;
	$tag == 0xFF and return $waypt;

	if ($tag == 1) {
	    $waypt->{shortname} = read_pstr();
	    next;
	}
	if ($tag == 2 or $tag == 3) {
	    $waypt->{description} = read_pstr();
	    next;
	}
	if ($tag == 5) {
	    $waypt->{notes} = read_pstr();
	    next;
	}
	if ($tag == 6) {
	    $waypt->{url_link_text} = read_pstr();
	    next;
	}
	if ($tag == 7) {
	    $waypt->{icon_descr} = read_pstr();
	    next;
	}
	if ($tag == 8) {
	    $waypt->{notes} = read_cstr();
	    next;
	}
	if ($tag == 9) {
	    $waypt->{url} = read_cstr();
	    next;
	}
	if ($tag == 0x10) {
	    $waypt->{url_link_text} = read_cstr();
	    next;
	}
	if ($tag == 0x63) {
	    $waypt->{latitude} = read_dbl();
	    next;
	}
	if ($tag == 0x64) {
	    $waypt->{longitude} = read_dbl();
	    next;
	}
	if ($tag == 0x65 or $tag == 0x66) {
	    $str = waypt_read(8);
	    next;
	}
	if ($tag == 0x84 or $tag == 0x85 or $tag == 0x86) {
	    $str = waypt_read(4);
	    next;
	}
	warn "Unknown tag $tag";
    }
}

# Read waypoints from an EasyGPS .loc file.
sub easygps_read_waypts {
    my @waypts;
    my $c;

    do {
	my $waypt = easygps_read_waypt();
	push @waypts, $waypt;

	$c = waypt_getc();
    } while defined($c) and not waypt_eof() and $c eq 'W';

    @waypts;
}

my $WPTCHARS = "0123456789ABCDEFGHJKMNPQRTVWXYZ";

# Convert a cache number to a shortname.
sub convertnum {
    my $cachenum = shift;
    if ($cachenum < 65536) {
	return sprintf("GC%X", $cachenum);
    }
    else {
	$cachenum += 16 * 31 * 31 * 31 - 65536;
	my $cachestr = '';	
	my $digit;

	while ($cachenum > 0) {
	    $digit = $cachenum % 31;
	    $cachestr = substr($WPTCHARS, $digit, 1) . $cachestr;
	    $cachenum = int($cachenum / 31);
	}

	return "GC$cachestr";
    }
}

# Get waypoints from a GPX .gpx file.
sub gpx_read_waypts {
    my $xml = shift;

    my @waypts;
    for my $w (@{$xml->{wpt}}) {
	my $waypt = {};
	$waypt->{shortname} = convertnum($w->{extensions}{cache}{id});
	$waypt->{notes} = $w->{extensions}{cache}{name}.' by '.
	    $w->{extensions}{cache}{placed_by};
	$waypt->{latitude} = $w->{lat};
	$waypt->{longitude} = $w->{lon};
	$waypt->{url_link_text} = $w->{link}{text};
	$waypt->{url} = $w->{link}{href};
	$waypt->{icon_descr} = $w->{sym};
	push @waypts, $waypt;
    }
    @waypts;
}

# Read waypoints from a Geocaching .loc file.
sub geo_read_waypts {
    my $xml = XMLin(waypt_getbuf(), ForceArray => ['waypoint', 'wpt']);

    # Detect GPX format and handle that.
    defined($xml->{wpt}) and return gpx_read_waypts($xml);

    my @waypts;
    for my $w (@{$xml->{waypoint}}) {
	my $waypt = {};
	$waypt->{shortname} = $w->{name}{id};
	$waypt->{notes} = $w->{name}{content};
	$waypt->{latitude} = $w->{coord}{lat};
	$waypt->{longitude} = $w->{coord}{lon};
	$waypt->{url_link_text} = $w->{link}{text};
	$waypt->{url} = $w->{link}{content};
	$waypt->{icon_descr} = $w->{type};
	push @waypts, $waypt;
    }
    @waypts;
}

# Output waypoints in Geocaching .loc file format.
sub geo_write_waypts {
    my $waypts = shift;

    my $xml = {};
    $xml->{loc}{version} = "1.0";
    $xml->{loc}{src} = "gcmerge";

    $xml->{loc}{waypoint} = [];
    
    for my $waypt (@$waypts) {
	my $w = {};
	$w->{name} = [ { 
	    id => $waypt->{shortname}, 
	    content => $waypt->{notes} 
	} ];
	$w->{coord} = [ { 
	    lat => $waypt->{latitude}, 
	    lon => $waypt->{longitude} 
	} ];
	$w->{link} = [ { 
	    text => $waypt->{url_link_text}, 
	    content => $waypt->{url} 
	} ];
	$w->{type}{content} = $waypt->{icon_descr};
	push @{$xml->{loc}{waypoint}}, $w;
    }

    print XMLout($xml, 
	XMLDecl => '<?xml version="1.0" encoding="UTF-8"?>', 
	KeepRoot => 1
    );
}

sub main {
    @ARGV >= 1 or die "Usage: $0 fname [fname...]\n";

    my @allwaypts;

    my %wayptorig;

    # In DOS, we have to do the file globbing ourselves.
    if ($^O eq "MSWin32" or $^O eq "dos" or $^O eq "os2") {
	@ARGV = map { File::DosGlob::glob $_ } @ARGV;
    }

    for my $arg (@ARGV) {
	waypt_open($arg);

	my @waypts;

	eval {
	    if (easygps_read_header()) {
		@waypts = easygps_read_waypts();
	    }
	    else {
		# If this file is not an EasyGPS file, 
		# try to parse it as a Geocaching .loc file.
		@waypts = geo_read_waypts();
	    }
	};
	if ($@) {
	    die "Failed to parse $arg: $@\n";
	}

	for my $waypt (@waypts) {
	    # Report duplicate waypoints. Merge the ones that aren't
	    # duplicates.
	    if (defined $wayptorig{$waypt->{shortname}}) {
		warn "Waypoint $waypt->{shortname} in file $arg ".
		    "was already in file $wayptorig{$waypt->{shortname}}\n";
	    }
	    else {
		push @allwaypts, $waypt;
		$wayptorig{$waypt->{shortname}} = $arg;
	    }
	}
    }

    warn "Writing ".scalar(@allwaypts)." waypoints...\n";
    geo_write_waypts \@allwaypts;
}

main;

__END__

Tags: easygps, geocaching, gps, perl
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 2 comments