eval 'exec perl -S $0 ${1+"$@"}' # -*-Perl-*-
if 0;
# File name: ''google2itn''
# Task: Conversion from Google Maps URL to TomTom Route
#
# State: under development
#
# DOC
# - see help text
#
# Notes
# -
#
# TODO
# - Need to parse new geocode tag, see e.g. Aoste Trip in last Biking tour
#
# created: 2008-04-27
# Version: $LastChangedRevision: 323 $
# last change: $LastChangedDate: 2009-03-15 21:16:19 +0100 (Sun, 15 Mar 2009) $
# (c) by Robert Lange (robert.lange@s1999.tu-chemnitz.de)
#
# License
# Google2itn
# Copyright (C) 2008 Robert Lange
# robert.lange@s1999.tu-chemnitz.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
# *** packes to use ***
use warnings;
use strict;
use Getopt::Long;
use FindBin; # Find local directory
use Encode; # Write Text for ITN as latin1
# HTTP Requester
use LWP::UserAgent;
use HTTP::Request::Common;
use URI::Escape;
# --------------------------------------------------------------------------------
# *** Global settings ***
# Program Name
our $Program_Name="google2itn";
# --------------------------------------------------------------------------------
# *** Global Variables ***
# Program Options
my %opts = (
# *** Settings from Command line
# ITN file name (command line)
itnfile => '',
# URL (command line)
url => '',
# Set to 1 for debug output (command line)
debug => 0,
# Set to 1 for reverse translation (ITN -> URL)
reverse => 0,
);
# ITN Entries: Array of Hashes
# Format of each Hash: long->longitude, lat->latitude, text->text description
# gc->geocode from URL (when available)
my @itn;
# --------------------------------------------------------------------------------
# *** subroutine declarations ***
# Main routine without any arguments
sub main ();
# Reads the command line options
sub read_commandline();
# Print help message
# return: help string
sub issue_help();
# Return version string from SVN revision & date
sub versionstring();
# Get places from URL
sub get_places();
# Parse point and put to hash
# 1.P: Reference for Value Hash (put result into)
# 2.P: string to parse
sub put_point(\%$);
# Convert coordinates to decimal format
# 1.P: GPS coordinates in this form: N 49°38.111 E 013°44.888
# return: coordinates in decimal format (50.351750, 12.311600)
sub gps_convert_to_dec($);
# Rount number to integer
sub round($);
# Ask for unknown places via URL request
sub fill_unknowns();
# Construct ITN
sub write_itn();
# Construct Google Maps URL from ITN file
sub write_url();
# Decode packed URL to readable format
sub url_decode($);
# --------------------------------------------------------------------------------
# *** "Body" of the program ***
main();
# --------------------------------------------------------------------------------
# *** Main routine ***
sub main() {
# *** Variables
# *** Read the command line
read_commandline();
# *** Reverse Operation: Now Parse ITN file ***
if ($opts{'reverse'}) { # Reverse translation requested
# *** Do _URL construction
write_url();
# *** Done
print "Done!\n";
return 0;
} # else ...
# *** Normal Operation: Now Parse URL ***
# *** Get places from URL
get_places();
# Deep DEBUG
# use Data::Dumper;
# print Dumper(@itn);
# exit 1;
# *** Ask for unknown places via URL request
fill_unknowns();
# Deep DEBUG
# use Data::Dumper;
# print Dumper(@itn);
# *** Construct ITN
write_itn();
# *** Done
print "Done!\n";
return 0;
}
# --------------------------------------------------------------------------------
# *** Get places from URL ***
sub get_places() {
# *** local variables
my $lurl; # local URL to work with
my $val; # Value on parse
my $has_to=0; # 1: URL has "to:" entries
my %pt; # Next ITN Point from URL
# Format of each Hash: long->longitude, lat->latitude,
# text->text description, gc->geocode from URL (when available)
my @gc; # Split geocodes from URL
my $count = 0; # Counter variable
# Template (line splits added)
# http://maps.google.de/maps?f=d&hl=de&geocode=18250402486037030471,50.351750,12.311600\
# %3B8010596657672648298,50.536260,12.531730&saddr=selb&daddr=markneukirchen\
# +to:K7841+%4050.351750,+12.311600+to:Sch%C3%B6neck,+vogtland+to:klingenthal\
# +to:sch%C3%B6nheide+to:Auerbacher+Stra%C3%9Fe%2FB169+%4050.536260,+12.531730\
# +to:bad+schlema+to:l%C3%B6%C3%9Fnitz+to:chemnitz&mra=pe&mrcr=4,5&\
# via=2,6&dirflg=h&sll=50.585308,12.667236&sspn=0.080982,0.166855&\
# ie=UTF8&ll=50.394512,12.637024&spn=1.300937,2.669678&z=9
#
# Only 2 items
# http://maps.google.de/maps?f=d&hl=de&geocode=&saddr=markneukirchen&\
# daddr=50.289833,12.35919&mra=dme&mrcr=0&mrsp=1&sz=14&sll=50.302395,12.346265\
# &sspn=0.040624,0.048065&ie=UTF8&z=14
#
# Geocode embedded example:
# http://maps.google.com/maps?f=d&saddr=K4&daddr=K6%2FNonnfelder+Hof+\
# to:50.851908,6.297913+to:Gressenicher+Str.%2FL11+to:J%C3%A4gerhausstra\
# %C3%9Fe%2FL24+to:Gr%C3%BCnentalstra%C3%9Fe%2FK21+to:B266&hl=de&geocode=\
# FT5LCgMdoDRdAA%3BFW7zCQMdyNheAA%3B%3BFWT2BgMdIO5fAA%3BFYDCBQMdWN5fAA%3B\
# FX6PAwMdVrNfAA%3BFZaVAwMdTEJiAA&mra=dpe&mrcr=0&mrsp=2&sz=10&via=1,2,3,4,5\
# &sll=50.776585,6.083613&sspn=0.613091,0.932465&ie=UTF8&z=10
print "Parsing URL ...\n";
# Shortcut
$lurl = $opts{'url'};
# *** saddr
$lurl =~ /saddr=(.+)&daddr=/ or die "Could not find saddr in URL!";
put_point(%pt, $1); # Parse point and put to hash
push @itn, { %pt }; # Put anonymous copy into resulting hash
# *** daddr
# Until next to: point?
if ($lurl =~ /daddr=(.+?)\+to:/) { # Yes
$has_to = 1;
put_point(%pt, $1); # Parse point and put to hash
push @itn, { %pt }; # Put anonymous copy into resulting hash
} else { # No to: points here
$lurl =~ /daddr=(.+?)&/ or die "Could not find daddr in URL!";
put_point(%pt, $1); # Parse point and put to hash
push @itn, { %pt }; # Put anonymous copy into resulting hash
}
# *** Point-by-point "to:" parsing
$lurl =~ /\+to:/g; # First, wind to initial "to:"
if ($has_to) { # Yes, items are there
while ($lurl =~ /(.+?)(\+to:|&)/g) { # For each match
put_point(%pt, $1); # Parse point and put to hash
push @itn, { %pt }; # Put anonymous copy into resulting hash
last if $2 eq "&"; # Abort condition
}
}
# Now, when available, add geocode values also here
# Template for geocode:
# &geocode=\
# FT5LCgMdoDRdAA%3BFW7zCQMdyNheAA%3B%3BFWT2BgMdIO5fAA%3BFYDCBQMdWN5fAA%3B\
# FX6PAwMdVrNfAA%3BFZaVAwMdTEJiAA&<...>
undef $_; # Empty before assignment
$_ = $1 if $opts{'url'} =~ /&geocode=([^&]+)&/;
if (defined $_ and $_ ne "") { # Yes, we found a geocode
print "DEBUG: get_places Got geocode \"$_\"\n" if $opts{'debug'};
@gc = split(/%3B/, $_, -1); # Split by separator
# -1: Do not strip trailing empty fields
die "Mitmatch in route point count for names and " . # Sanitiy check
"geocode (ITN=$#itn vs. GC=$#gc)!? Please inform author."
if $#itn ne $#gc;
foreach (@gc) { # Append geocode to all route points
$itn[$count]->{gc} = $_;
$count++;
}
}
# Done with work
return;
}
# *** Parse point and put to hash ***
# 1.P: Reference for Value Hash (put result into)
# 2.P: string to parse
sub put_point(\%$) {
# *** local variables
my ($ptref, $in) = @_;
my ($txt, $cord); # Text and coordinates
print "DEBUG: put_points: Got text \"$in\"\n" if $opts{'debug'};
# Clear up hash first
undef %$ptref;
# Convert URL strings to readable text
$in = url_decode($in);
print "DEBUG: put_points: Converted text \"$in\"\n" if $opts{'debug'};
# Following text prototypes are needed:
# (A)
# New since E/2008: N 49°38.111 E 013° 44.888
# (B)
# K7841 woauchimmer @50.351750, 12.311600
# markneukirchen woauchimmer
# 50.289833,12.35919
$txt = ''; $cord = ''; # Fill empty
if ( $in =~ /^[NS]\s+\d+°\s*[\d\.]+\s+[EW]\s+\d+°\s*[\d\.]+$/ ) { # (A) Parsing for normal geocoordinates
$txt = ""; # No text found
# Convert coordinates to standard format
$cord = gps_convert_to_dec($in); # Convert coordinates to decimal format
} else { # (B) Default parsing
$in =~ /^(.*?)\s*\@?([\d\.]+,\s*[\d\.]+)?$/;
$txt = $1;
$cord = $2;
}
if (! $txt ) {
$txt = 'Unknown'; # Assign "empty" text
}
if ( ! $txt and ! $cord ) {
die "Could not understand waypoint \"$in\"";
}
# Fill fields: Text
$$ptref{'text'} = $txt if $txt;
# Fill fields: Coordinates (when available)
if ($cord) {
$cord =~ /^\s*([\d\.]+),\s*([\d\.]+)\s*$/
or die "Could not understand coordinates \"$cord\"";
$$ptref{'lat'} = round($1 * 100000); # In TomTom format
$$ptref{'long'} = round($2 * 100000); # In TomTom format
}
# Over and out
return;
}
# Convert coordinates to decimal format
# 1.P: GPS coordinates in this form: N 49°38.111 E 013°44.888
# return: coordinates in decimal format (50.351750, 12.311600)
sub gps_convert_to_dec($) {
# *** Parameters and variables
my ($in) = @_;
my ($ns, $longD, $longM, $ew, $latD, $latM); # Store values from input
my ($long, $lat); # Calculated values
# Fetch all values
$in =~ /^([NS])\s+(\d+)°\s*([\d.]+)\s+([EW])\s+(\d+)°\s*([\d.]+)$/
or die "Cound not understand degrees-minute format \"$in\"";
$ns = $1;
$longD = $2;
$longM = $3;
$ew = $4;
$latD = $5;
$latM = $6;
# Calculate Longitude
$long = $longD + 1.0*$longM/60;
if ( $ns eq "S") {
$long *= -1;
}
# Calculate Latitude
$lat = $latD + 1.0*$latM/60;
if ( $ew eq "W") {
$lat *= -1;
}
print "DEBUG: gps_convert_to_dec: Converted coordinates \"$long, $lat\"\n"
if $opts{'debug'};
# Return
$long . ", " . $lat;
}
# *** Rount number to integer ***
sub round($) {
my ($number) = @_;
return int($number + .5 * ($number <=> 0));
}
# --------------------------------------------------------------------------------
# *** Ask for unknown places via URL request ***
sub fill_unknowns() {
# *** local variables
my ($txt, $gc); # Text, Geocode
my ($src, $dest, $geo, $gurlgc); # For request with geocode
my $ua = new LWP::UserAgent; # New HTTP request object
my $google_url; # Use same google url as given on command line
my $google_url_gc; # Use same google request
my $try_geocode; # True: Try geocode processing
my $greq; # Googe Request result
my ($long, $lat); # Coordinates
my $kml_string; # KML string (separate from $greq to allow multiple matching)
my ($pmk, $pmk_pre); # Placemark - tag parsing (current and last value)
print "Fetching unknown coordinates ...\n";
# Get google URL for normal request
$opts{'url'} =~ m|^\s*((:?http://)?[^/]+)/|
or die "Could not isolate google URL from input URL?!?";
$google_url = $1;
# Get google URL for geocode request, with blanked saddr, daddr and geocode
$google_url_gc = $opts{'url'};
$google_url_gc =~ s/&saddr=[^&]*&/&saddr=&/; # blank saddr
$google_url_gc =~ s/&daddr=[^&]*&/&daddr=&/; # blank daddr
$google_url_gc =~ s/&geocode=[^&]*&/&geocode=&/; # blank geocode
# For all missing coordinates fetch them via google
foreach my $ptref (@itn) {
if (! defined( ${$ptref}{'long'} ) or ! defined(${$ptref}{'lat'}) ) { # Fetch it
$txt = $$ptref{'text'}; # Shortcut
$gc = $$ptref{'gc'}; # Shortcut 2
$try_geocode = 0; # Reset flag
undef $long; undef $lat; # Reset result variables
print " $txt";
print " (Geocode $gc)" if $gc;
print "\n";
print "$google_url/maps?f=q&hl=de&geocode=&q=". uri_escape($txt) . "&ie=UTF8&output=kml\n"
if $opts{'debug'};
$greq = $ua->request(GET "$google_url/maps?f=q&hl=de&geocode=&q=" . uri_escape($txt) . "&ie=UTF8&output=kml");
die "Could not fetch Data from google:" . $greq->error_as_HTML
unless $greq->is_success; # Error check
# Try to extract values from KML file the traditional way (w/o geocode)
if ( $greq->content =~ m|(-?[\d\.]+)| ) {
$long = $1;
} else { # Fail, need to try geocode decoding
$try_geocode = 1;
}
if ( $greq->content =~ m|(-?[\d\.]+)| ) {
$lat = $1;
} else { # Fail, need to try geocode decoding
$try_geocode = 1;
}
if ( $try_geocode ) { # Failure? Try with geocode as 2nd option
if ( $opts{'debug'} ) {
print "Failing location parsing in KML, ";
print "re-try with geocode KML\n";
}
# For this trial, take input URL, replace source, destination and geocode
$gurlgc = $google_url_gc; # Make copy, to replace values inside
# saddr: Just take first point
$src=$itn[0]->{'text'};
# daddr: Our target
$dest=$txt;
# geocode of s+daddr
$geo = $itn[0]->{'gc'} . "%3B" . $gc;
# Insert values into URL
$gurlgc =~ s/&saddr=&/&saddr=$src&/;
$gurlgc =~ s/&daddr=&/&daddr=$dest&/;
$gurlgc =~ s/&geocode=&/&geocode=$geo&/;
$gurlgc .= "&output=kml"; # Request kml output
$greq = $ua->request(GET $gurlgc);
print "$gurlgc\n"
if $opts{'debug'};
die "Could not fetch Data from google (2): " . $greq->error_as_HTML
unless $greq->is_success; # Final failure
# Now decode $dest, for coordinate extraction
$dest = url_decode($txt);
# There are another special replacements - do them
$_ = $dest;
s/Ö/O/g; s/Ä/a/g; s/Ü/u/g; s/ö/o/g; s/ä/a/g; s/ü/u/g; # German Umlauts
s/ß/ss/g; # ß
$dest =$_;
print "Decoded address to look for: $dest\n"
if $opts{'debug'};
# Extract coordinates
# Template
# Ankommen in: Gressenicher Str./L11\
# Gressenicher Str./L11\
# root://styleMaps#default+nicon=0x467+hicon=0x477\
# 6.286880,50.787940,0
# Separate by tags
$kml_string = $greq->content; # Save, otherwise m|||g will loop at 1st match
#pmk = ""; # Initially empty
while ( $kml_string =~ m|(.*?)|ig ) {
$pmk_pre = $pmk; # Save last fetched value
$pmk = $1;
if ( $pmk =~ m|$dest|i ) { # We found the address
$pmk =~ m|\s*(-?[\d\.]+)\s*,\s*(-?[\d\.]+)\s*(?:,\d+)?|i
or die "Could not extract data (3) from KML for \"$dest\":\n"
. $greq->content;
$long = $1;
$lat = $2;
last; # Now we can exit here
}
}
if ( !$lat or !$long ) {
# Sometimes, out of whatever stupid reason, the name in the
# tag is changed from the initial name :-((
# In this case, as a last resort, try to fetch the second-last
# entry and hope it is what we're looking for
if ($pmk_pre =~ m|\s*([\d\.]+)\s*,\s*([\d\.]+)\s*(?:,\d+)?|i ) {
# Just take the match we found
$long = $1;
$lat = $2;
print "WARNING - Guessing decoding value in KML file because address tag was not found!\n"
}
}
# Final result check
if ( !$lat or !$long ) {
die "Could not extract data from google geocode request:\n" . $greq->content;
}
}
# Assign back
$$ptref{'long'} = round($long * 100000); # In TomTom format
$$ptref{'lat'} = round($lat * 100000); # In TomTom format
}
}
}
# --------------------------------------------------------------------------------
# *** Construct ITN ***
sub write_itn() {
# *** local variables
my $idx = 0; # thread first and last entry special
print "Writing output file $opts{'itnfile'} ...\n";
open OUT, ">", $opts{'itnfile'} or
die "Could not open output file $opts{'itnfile'}: $?";
# Parse ITN file for coordinates and locations
foreach (@itn) {
# Print Text - It must be in latin1 encoding
print OUT "$$_{'long'}|$$_{'lat'}|" .
encode("iso-8859-1", decode("utf-8", $$_{'text'}));
# Last is type 2, others are type 0
if ($idx == $#itn) {
print OUT "|2|\n";
} else {
print OUT "|0|\n";
}
# Increment index
$idx++;
}
# Done
close OUT;
return;
}
# --------------------------------------------------------------------------------
# *** Construct Google Maps URL from ITN file ***
sub write_url() {
# *** local variables
my @points; # ITN points in Format "K7841 woauchimmer @50.351750, 12.311600"
my $idx = 0; # Special handling for first two
my $url = ''; # Resulting URL
print "Generate Google Maps URL from TomTom Route file $opts{itnfile} ...\n";
open IN, "<", $opts{'itnfile'} or
die "Could not open input file $opts{'itnfile'}: $?";
# *** Read ITN
foreach my $line () {
# The text is in latin1 encoding, but UTF8 is expected
$line = encode("utf-8", decode("iso-8859-1", $line) );
# Template: 602541|4404970|Avenue de Haute Provence/N85|0|
$line =~ /^\s*(-?\d+)\|(-?\d+)\|(.*)\|\d+\|\s*$/
or die "Could not parse TomTom ITN line \"$line\"";
push @points, "$3 \@" . $2/100000 . ", " . $1/100000;
}
# Deep debug
# use Data::Dumper;
# print Dumper(@points);
# Error check: Single lines not supported
if ($#points < 1) {
die "Single line (or empty) ITN files not supported (yet)";
}
# *** Construct URL Template:
# http://maps.google.de/maps?f=d&source=s_d&saddr=Bad+M%C3%BCnstereifel+\
# %4050.55384,+6.75936&daddr=m%C3%B6nchengladbach++%4051.191260,6.442070&hl=en&\
# geocode=&mra=ls&sll=51.191394,6.442065&sspn=0.009844,0.009313&g=\
# m%C3%B6nchengladbach+&ie=UTF8&z=9
# Prefix
$url = "http://maps.google.de/maps?f=d&source=s_d";
# Points
foreach (@points) {
if ($idx == 0) { # First Item
$url .= "&saddr=";
} elsif ($idx == 1) { # Second Item
$url .= "&daddr=";
} else { # All other items
$url .= "+to:";
}
# Encoded location string
print "DEBUG: write_url: Parsed ITN item \"$_\"\n" if $opts{'debug'};
tr/ /+/; # Spaces to Plus
s/@/\%40/g; # Encode @
# And all remaining special characters must be encoded
s/([^A-Za-z0-9\%,\.\+])/sprintf("%%%02X", ord($1))/seg;
print "DEBUG: write_url: Converted text \"$_\"\n" if $opts{'debug'};
$url .= $_;
$idx++; # Next line is to be done
}
# Postfix
$url .= "&hl=en&ie=UTF8";
# And print it
print "\n$url\n\n";
# Done
close IN;
return;
}
# --------------------------------------------------------------------------------
# *** Decode packed URL to readable format ***
sub url_decode($) {
$_ = shift;
$_ = uri_unescape($_);
tr/+/ /; # Special: Decode spaces
return $_;
}
# --------------------------------------------------------------------------------
# *** reads the command line options ***
sub read_commandline() {
# *** local variables
my $help; # set to one if help screen is requested
my $ver; # set to one if version is requested
Getopt::Long::Configure ("bundling"); # enable bundling level 1
my $result = GetOptions(\%opts,
'debug|d!',
'reverse|r!',
'help|h|?' => \$help,
'version|V' => \$ver
);
if (! $result ) {
print STDERR "\nFailed to parse the command line options: Exiting\n";
exit 1;
}
if ( $ver ) {
print "\n" . $Program_Name . ", revision " . versionstring() . "\n";
exit 0;
}
if ( $help ) {
print issue_help();
exit 0;
}
if ($opts{'reverse'}) { # Reverse translation requested
if (scalar(@ARGV) != 1) {
# There were no arguments or too many
print STDERR "Reverse Translation: Input file expected!\n" .
"See Help message for information\n";
exit 1;
}
# assign arguments from @ARGV
$opts{'itnfile'} = $ARGV[0];
} else { # Normal operation
if (scalar(@ARGV) != 2) {
# There were no arguments or too many
print STDERR "Output file and URL expected! See Help message for information\n";
exit 1;
}
# assign arguments from @ARGV
$opts{'itnfile'} = $ARGV[0];
$opts{'url'} = $ARGV[1];
}
return;
}
# *** Return version string from SVN revision & Date ***
sub versionstring() {
# *** Vars
my $rev = '$LastChangedRevision: 323 $';
my $date = '$LastChangedDate: 2009-03-15 21:16:19 +0100 (Sun, 15 Mar 2009) $';
# Filter revision
$rev = $1 if $rev =~ /LastChangedRevision: (\d+)/;
# Filter date
$date = $1 if $date =~ /LastChangedDate: ([\d-]+)/;
"$Program_Name revision $rev from $date \n"
. 'Copyright (C) 2008,2009 Robert Lange (robert.lange@s1999.tu-chemnitz.de)' . "\n"
. "Licensed under the GNU General Public License\n";
}
# --------------------------------------------------------------------------------
# *** Print Help message ***
# returns help string
sub issue_help() {
# *** Help Text
my $Help_Documentation=<
Homepage http://sethdepot.org/site/Main/Google2Itn
EOF
# return help string
"\n" . versionstring() . "\n" .
$Help_Documentation;
};