#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use File::Spec;
use Math::Trig;
use POSIX qw(floor);
################################
# User Configurable Parameters #
################################
# Where we store the image cache
my $cache = File::Spec->catfile('.', 'cache');

my $debug = 1;
################################
# Global Variables used        #
################################
# Type of map to create.
my $satellite;
my $regular;
my $center;

# Upper Left tile. For a regular map, this should be set to the X
# and Y of the image in the upper left corner. For a satellite map, this
# should be set to the can qrst string. The aardvark extension for
# Firefox makes finding this quite easy
# Note: Pico Blvd & S. Beverly Dr 90035 is 5607,13083
# RBS is  "trttrsqqqrrstqqqsr"
my $x0;
my $y0;
my $goog0;
my $lat;
my $long;

# Allow a user to specify an offset from the initial $goog0 or $lat/$long. That way, one
# doesn't have to spend all day trying to find the perfect tile. A
# positive X and Y mean move the start square down and to the right,
# respectively (in other words, the image shifts up and to the left)
my $x_init_offset;
my $y_init_offset;

# (Default) size(s) of the map to generate
my $width  = 5;
my $height = 5;
my $zoom   = 15;

# Disable buffering, so the status is updated properly
$| = 1;

# Index from 0 to $width/$height
my $yoffset;
my $xoffset;

# Current position
my $x;
my $y;

# Complete URL to fetch
my $url;

# Which google server to use
my $site;

# Path at the server. Also, the local name from wget
my $path;

# Nice name, reflecting the $x and $y
my $clean_name;

# Self evident
my $command;
my $progress;
my $deletecache;
my $show_help;

# File extension. Regular maps are GIF, Satellite are JPG
my $suffix;

# Help distribute the load for satellite images. Not used for regular maps
my $currentserver = 0;


################################
# Actual Code                  #
################################
GetOptions(
  "height=i"  => \$height,
  "width=i"   => \$width,
  "x0=i"      => \$x0,
  "y0=i"      => \$y0,
  "goog0=s"     => \$goog0,
  "satellite" => \$satellite,
  "regular"   => \$regular,
  "dx=i"      => \$x_init_offset,
  "dy=i"      => \$y_init_offset,
  "lat=f"     => \$lat,
  "long=f"    => \$long,
  "zoom=i"    => \$zoom,
  "clean"     => \$deletecache,
  "center"    => \$center,
  "help|?"    => \$show_help
  )
  or show_help();
show_help() if ($show_help);
printf("Google Maps Tiler v2.0, by Mike Miller\n");
if ($regular) {
  if ($x_init_offset) { $x0 += $x_init_offset; }
  if ($y_init_offset) { $y0 += $y_init_offset; }
  if ($center)        {
    $x0 -= floor($width / 2);
    $y0 -= floor($height / 2);
  }
  die "Regular maps must have an x0 and y0 specified!\n" if (!$x0
    || !$y0);
  printf("Creating a regular map from %d,%d to %d,%d\n", $x0, $y0, $x0 + $width - 1, $y0 + $height - 1);
  $suffix = "gif";
} elsif ($satellite) {
  if (!$goog0 && $lat && $long) {
    $goog0 = ll2goog($long, $lat, $zoom);
  } elsif ($goog0) {
  } else {
    die "Satellite maps must have either goog0 or lat/long coordinates!\n";
  }

  # (Re)calculate zoom based on goog0
  $zoom = length($goog0);
  ($x0, $y0) = goog2xy($goog0);

  # Various adjustments
  if ($x_init_offset) { $x0 += $x_init_offset; }
  if ($y_init_offset) { $y0 += $y_init_offset; }
  if ($center)        {
    $x0 -= floor($width / 2);
    $y0 -= floor($height / 2);
  }

  #Recompute in case the offsets or center moved us
  $goog0 = xy2goog($x0, $y0, length($goog0));
  my $googEnd = xy2goog($x0 + $width - 1, $y0 + $height - 1, length($goog0));
  printf(
    "Creating a satellite map from %s (%d,%d) to %s (%d,%d) with zoom $zoom\n",
    $goog0, $x0, $y0, $googEnd,
    $x0 + $width - 1,
    $y0 + $height - 1
  );
  $suffix = "jpg";
} else {
  die "You have to specify either a satellite or regular map!\n";
}
mkdir($cache, 0755) unless (-d $cache);
die "Error: $cache isn't writeable!" unless (-w $cache);

# Loop over Y first, then X
for ($yoffset = 0 ; $yoffset < $height ; $yoffset++) {
  for ($xoffset = 0 ; $xoffset < $width ; $xoffset++) {
    $x          = $x0 + $xoffset;
    $y          = $y0 + $yoffset;
    $clean_name = "Tile$x,$y.$suffix";
    $progress   = int(100 * ($yoffset * $width + $xoffset) / ($width * $height));
    printf("Fetching %05d,%05d\t(%02d%% completed)\r", $x, $y, $progress);
    get_tile($clean_name, $x, $y, length($goog0));
  }
}
printf("Fetching %05d,%05d\t(100%% completed)\n", $x0 + $width - 1, $y0 + $height - 1);
printf("All Tiles Retreived\n");
for ($yoffset = 0 ; $yoffset < $height ; $yoffset++) {
  $command = "convert ";
  for ($xoffset = 0 ; $xoffset < $width ; $xoffset++) {
    $x          = $x0 + $xoffset;
    $y          = $y0 + $yoffset;
    $clean_name = File::Spec->catfile($cache, "Tile$x,$y.$suffix");
    $command .= "$clean_name ";
  }
  $command .= "+append " . File::Spec->catfile($cache, "tmp$yoffset.$suffix");    # The +append means horizontal tiling
  printf("Creating horizontal strip %d/%d\r", $yoffset + 1, $height);
  system($command);
}
$command = "convert ";
for ($yoffset = 0 ; $yoffset < $height ; $yoffset++) {
  $command .= File::Spec->catfile($cache, "tmp$yoffset.$suffix") . " ";
}
$command .= "-append map.$suffix";                                                # The -append means vertical tiling
printf("\nCreating output image\n");
system($command);
printf("Cleaning up strips.\n");
system("rm " . File::Spec->catfile($cache, "tmp*"));
if ($deletecache) {
  printf("Deleting the cache.\n");
  system("rm -rf " . $cache);
}
printf("File map.$suffix created. Enjoy.\n");
#####################
# Support Functions #
#####################
sub goog2xy {
  my ($goog) = @_;
  $_ = $goog;
  s/q/0/g;
  s/r/1/g;
  s/s/1/g;
  s/t/0/g;
  my $hst = $_;
  $_ = $goog;
  s/q/0/g;
  s/r/0/g;
  s/s/1/g;
  s/t/1/g;
  my $vst  = $_;
  my $hval = oct("0b" . $hst);
  my $vval = oct("0b" . $vst);
  return $hval, $vval;
}

sub ll2goog {
  my ($long, $lat, $zoom) = @_;

  # Longitude is simple in this projection.
  my $x = (180 + $long) / 360;

  # Convert to radians
  my $y = ($lat) * (pi / 180);

  # This corrects for the errors in the projection
  $y = 0.5 * log((1 + sin($y)) / (1 - sin($y)));

  #  Normalize
  $y /= (2 * pi);
  $y -= 0.5;
  $y = 1 - $y;
  my $scale = 2**($zoom);
  $x *= $scale;
  $x /= 2;
  $y *= $scale;
  $x = int($x);
  $y = int($y);
  my $goog = xy2goog($x, $y, $zoom);
  return $goog;
}

sub xy2goog {
  my ($x, $y, $zoom) = @_;
  my @quadChars = qw/q t r s/;
  my $format    = '%0' . $zoom . 'b';
  my @xBits     = split(//, sprintf($format, $x));
  my @yBits     = split(//, sprintf($format, $y));
  my $res       = '';
  for (my $i = 0 ; $i < $zoom ; $i++) {
    $res .= $quadChars[ 2 * ($xBits[$i]) + $yBits[$i] ];
  }
  return $res;
}

sub get_tile {
  my ($clean_name, $x, $y, $zoom) = @_;
  my $google;
  if (!(-s File::Spec->catfile($cache, $clean_name)))    # if it exists && non zero
  {
    if ($regular) {
      $site = "http://mt.google.com/";
      $path = "mt?n=404&v=w2.10&x=$x&y=$y&zoom=2";
    } elsif ($satellite) {
      $currentserver++;
      if ($currentserver == 4) {
        $currentserver = 0;
      }
      $site   = "http://kh" . $currentserver . ".google.com/";
      $google = xy2goog($x, $y, $zoom);
      $path   = "kh?n=404&v=9&t=$google";
    } else {
      die "Unexpected error! Help!!!!\n";
    }
    $url = $site . $path;
    print "\nRequesting $url..." if $debug;
    # Use windows even under linux to force a known renaming scheme
    my $rc=system("wget --restrict-file-names=windows -q \"$url\"");
    print "Returned $rc..." if $debug;
    $path =~ s/\?/\@/g;
    if (-e "$path") {
      print "renamed to $clean_name\n" if $debug;
      system("mv \"$path\" " . File::Spec->catfile($cache, $clean_name));
    } else {
      if ($satellite) {
        print "Not found. Moving up...\n" if $debug;
        # We can interpolate satellite pictures
        my $direction = chop($google);
        ($x, $y) = goog2xy($google);
        my $parent_name = "Tile$x,$y.$suffix";
        get_tile($parent_name, $x, $y, length($google));
        if ($direction eq "q") {
          $command = "convert "
            . File::Spec->catfile($cache, $parent_name)
            . " -crop 128x128+0+0 -resize 256x256 "
            . File::Spec->catfile($cache, $clean_name);
        } elsif ($direction eq "r") {
          $command = "convert "
            . File::Spec->catfile($cache, $parent_name)
            . " -crop 128x128+128+0 -resize 256x256 "
            . File::Spec->catfile($cache, $clean_name);
        } elsif ($direction eq "t") {
          $command = "convert "
            . File::Spec->catfile($cache, $parent_name)
            . " -crop 128x128+0+128 -resize 256x256 "
            . File::Spec->catfile($cache, $clean_name);
        } elsif ($direction eq "s") {
          $command = "convert "
            . File::Spec->catfile($cache, $parent_name)
            . " -crop 128x128+128+128 -resize 256x256 "
            . File::Spec->catfile($cache, $clean_name);
        } else {
          die "FATAL ERROR.. invalid direction! (This should never happen...)\n";
          system("convert -size 256x256 xc:green " . File::Spec->catfile($cache, $clean_name));
        }
        system($command);
      }
    }
  }
}

sub show_help {
  print <<EOF;
Google Maps Tiler v2.0, by Mike Miller

Available options (all can be abbreviated):
-height\t\t- Height in Tiles Default 5
-width\t\t- Width in Tiles. Default 5
-satellite\t- Select a satellite image
-regular\t- Select a standard (street) map
-x0\t\t- X index for Regular maps
-y0\t\t- Y index for Regular maps
-goog0\t\t- "qrst" string indicating the Upper Left for Satellite maps
-lat\t\t- Latitude (rounded to the nearest tile)
-long\t\t- Longitude (rounded to the nearest tile)
-center\t- Centered on the given coordinate
-dx\t\t- For satellite, allows a horizontal shift from goog0. Pos pans W
-dy\t\t- For satellite, allows a vertical shift from goog0. Pos pans S
-help or-?\t- Displays this help

EOF
  exit;
}

