Google Map Tile Stitching

I’ve made a Google Aerial Map Tile Stitcher in PHP.

I prefer to be a legal type.  No, I don’t wear a cape and a wig, or any of that shennanigans.  But I like to pay for things that I value.  Music, software, videos – I do it all. 

So, to produce a nicely printed aerial-view map for my wall, I attempted to purchase a copy.  Having been quoted more than £1000 for the honour of a picture of my local river (the beautiful, if rather muddy, River Deben near Ipswich), I decided that honourable intent drew the line somewhere.

So I put together a program to download a Google Aerial Map.  They have some really nice imagery, though for London, it doesn’t beat Microsoft’s, especially with the multi-angle view.  But they have a nice shot of the river.

The tiles are rather oddly named, and are a fun exercise in coding to iterate across a grid.  I won’t explain it here, since people do so better elsewere, but I attach my solution.  It takes a tile code to start, and a number of tiles across and down to build the picture.

Run from the command line and it won’t time-out on you.

 Now, there are some important things to note here.

  1. I could have gone to a higher resolution, but the extra accuracy wouldn’t have shown for the area I needed.  On a 300dpi one-metre-wide printer, I didn’t need the last two levels of accuracy.  This saves a *lot* of downloading.
  2. The last level of accuracy doesn’t seem to reveal any extra detail – I suspect it’s artifically grown, so there’s no real need for the last level at all (bit of a cheat really).
  3. Google will block requests from your IP after a certain number of tile downloads.  I discovered this was irrespective of any artificial delays I added to simulate user behaviour.  You can revalidate by manually entering a string.  However, if you hit this limit, you’re essentially pissing off Google and getting too much to be useful anyway.  Use a lower resolution (shorter code string), and download less tiles.
  4. Add a Google logo and copyright statements to your final image and only use it for personal stuff.  Unlike me, big companies like the whole capes-and-wigs thing.  Now you’re only using freely-available content.  Guilt-free :)
  5. Google downloads tiles from four different domain names, called kh0 through kh3.  Interesting.  I assumed this was to reduce load, and simulated the random selection of a server (attempting to simulate user activity).  It doesn’t help with the blocking, or the server load (you’re only downloading static images anyway).  So why is it there?

The maps downloads from four domains to fool your browser into allowing extra threads, thereby increasing the speed of small downloads.  This is a nice idea.  Your browser will only load about 4 items at any one time from a single domain, so if you’re loading many small items, you generate a queue.  Google gets around that by using different subdomains.  No cookies or code are downloaded, so it doesn’t hamper any security systems. It’s a neat trick.

<?php
ini_set('memory_limit', '1500M');
class megaMap {
//constructor params
 var $topLeftCode = "t";
 var $width = 1;
 var $height = 1;
 var $filename = "images/map";
 var $filetype = "jpg";
//constants
 var $tileWidth = 256;
 var $tileHeight = 256;
 
//var $mapServers = array("http://local/images/tempimage.jpg?");
 var $mapServers = array("http://kh0.google.co.uk/kh?n=404&v=23&t=", "http://kh1.google.co.uk/kh?n=404&v=23&t=", "http://kh2.google.co.uk/kh?n=404&v=23&t=", "http://kh3.google.co.uk/kh?n=404&v=23&t=");
 var $tileIds = array("q", "r", "t", "s");
 var $quality = 99;
//internal
 var $progress = 0;
  var $img = 0;
  var $initX = 0;
  var $initY = 0;
  var $tilesDir = 'images/tiles/';
  var $cacheDir = 'images/tiles/';
  function megaMap ($topLeftCode, $width, $height, $filename, $filetype, $progress = 0) {
  $this->topLeftCode=$topLeftCode;
  $this->width=$width;
  $this->height=$height;
  $this->filename=$filename;
  $this->filetype=$filetype;
  $this->progress=$progress;
  if ($progress>0) {
  echo "restoring from ".$this->filename.$progress.'.jpg'." at point ".$progress."\n";
  $this->img=@imagecreatefromjpeg($this->filename.$progress.'.jpg');
  } else {
  $this->img=$this->newCanvas();
  }
  $this->updateFromProgress();
  }
//newCanvas
  function newCanvas() {
  return @imagecreatetruecolor ($this->width * $this->tileWidth, $this->height * $this->tileHeight);
  }
//updateFromProgress
 function updateFromProgress() {
  $this->initX = $this->progress % $this->width;
  $this->initY = (int) ($this->progress / $this->width);
 }

//startTiling
 function startTiling() {
  $heightTileCode = $this->topLeftCode;
  for ($i=0;$i<$this->height;$i++) {
  $widthTileCode = $heightTileCode;
  for ($j=0;$j<$this->width;$j++) {
  if ($i<$this->initY || ($i == $this->initY && $j<$this->initX)) {
  continue;
  }

$tileImage = $this->getTile($widthTileCode);
  $this->addTile($tileImage, $j, $i);
  imagedestroy($tileImage);

$this->outputStatus();
  $this->progress++;
  $widthTileCode = $this->getNextTileCodeRight($widthTileCode);
  }
  $heightTileCode = $this->getNextTileCodeDown($heightTileCode);
  if (($i % 10) == 0) $this->saveImage();
  }
  $this->saveImage();
 }

//getNextTileCodeRight
 function getNextTileCodeRight($tileCode) {
  if ($tileCode=='') return $tileCode;
  if (substr($tileCode, -1)=='q') {
  return substr($tileCode, 0, -1) . 'r';
  } else if (substr($tileCode, -1)=='t') {
  return substr($tileCode, 0, -1) . 's';
  } else return $this->getNextTileCodeRight(substr($tileCode, 0, -1)) . $this->getNextTileCodeLeft(substr($tileCode, -1));
 }

//getNextTileCodeLeft
 function getNextTileCodeLeft($tileCode) {
  if ($tileCode=='') return $tileCode;
  if (substr($tileCode, -1)=='r') {
  return substr($tileCode, 0, -1) . 'q';
  } else if (substr($tileCode, -1)=='s') {
  return substr($tileCode, 0, -1) . 't';
  } else return $this->getNextTileCodeLeft(substr($tileCode, 0, -1)) . $this->getNextTileCodeRight(substr($tileCode, -1));
 }

//getNextTileCodeDown
 function getNextTileCodeDown($tileCode) {
  if ($tileCode=='') return $tileCode;
  if (substr($tileCode, -1)=='q') {
  return substr($tileCode, 0, -1) . 't';
  } else if (substr($tileCode, -1)=='r') {
  return substr($tileCode, 0, -1) . 's';
  } else return $this->getNextTileCodeDown(substr($tileCode, 0, -1)) . $this->getNextTileCodeUp(substr($tileCode, -1));
 }

//getNextTileCodeUp
 function getNextTileCodeUp($tileCode) {
  if ($tileCode=='') return $tileCode;
  if (substr($tileCode, -1)=='t') {
  return substr($tileCode, 0, -1) . 'q';
  } else if (substr($tileCode, -1)=='s') {
  return substr($tileCode, 0, -1) . 'r';
  } else return $this->getNextTileCodeUp(substr($tileCode, 0, -1)) . $this->getNextTileCodeDown(substr($tileCode, -1));
 }

//getTile
 function getTile($tileCode) {
  //attempt to retrieve from the cache
  $tileImage=@imagecreatefromjpeg($this->cacheDir.$tileCode.'.jpg');
  if ($tileImage) {
// echo "using cache\n";
  } else {
  //get server
  srand((double)microtime()*1000000);
  $attempt = 0;
  while ($attempt<4 && !$tileImage) {
// if ($attempt>0) sleep(600);
  $server = $this->mapServers[rand(0, count($this->mapServers)-1)];
  //get filestring
  $fileString = $server.$tileCode;
  echo $fileString."\n";
  //get image
  $tileImage=@imagecreatefromjpeg($fileString);
  $attempt++;
// sleep(rand(0,5));
  }
  if (!$tileImage) {
  $this->saveImage();
  die ("Failed to receive tile at " . $this->progress);
  }
  //save to cache
  imagejpeg($tileImage, $this->tilesDir.$tileCode.".jpg", $this->quality );
  }
  //return
  return $tileImage;
 }

//addTile
 function addTile($tileImage, $tileX, $tileY) {
  $tileDestX = $tileX * $this->tileWidth;
  $tileDestY = $tileY * $this->tileHeight;
  imagecopy ( $this->img, $tileImage, $tileDestX, $tileDestY, 0, 0, $this->tileWidth, $this->tileHeight);
 }

//saveImage
 function saveImage() {
  imagejpeg($this->img, $this->filename.$this->progress.".jpg", $this->quality );
 }

//outputStatus
 function outputStatus() {
  echo $this->progress."\n";
  flush();
 }

}

$topLeftCode = "trtqtqtqqrsstrrrqqq";
$width = 70;
$height = 64;
$filename = "images/map";
$filetype = "jpg";
$progress = 0; // start at the beginning

$img = new megaMap($topLeftCode, $width, $height, $filename, $filetype, $progress);
$img->startTiling();

?>
This entry was posted in Projects and tagged , , . Bookmark the permalink.
  • Alex Sosa

    Hi Ken,

    Found your Google Map Tile Stitching article while trying to accomplish same using Visual FoxPro. I ‘ve already found out how to get the tiles and am trying to figure out how to stitch them together. Since I am not familiar with PHP it is hard to understand how you join two files to make a single image. Are they simply concatenated? How is position of a tile indicated? The tiles I downloaded are PNGs, not JPGs.

    BTW, try deleting the Google cookies to get longer time.

    Thanks a lot,

    Alex

  • Alex Sosa

    Hi Ken,

    Found your Google Map Tile Stitching article while trying to accomplish same using Visual FoxPro. I ‘ve already found out how to get the tiles and am trying to figure out how to stitch them together. Since I am not familiar with PHP it is hard to understand how you join two files to make a single image. Are they simply concatenated? How is position of a tile indicated? The tiles I downloaded are PNGs, not JPGs.

    BTW, try deleting the Google cookies to get longer time.

    Thanks a lot,

    Alex

  • http://kenneth.kufluk.com/ Kenneth

    Line 150 adds the tile into a larger canvas.

    I don’t believe PHP was accepting cookies, so that won’t matter. Google will block your IP, which won’t make you popular at work :)

    Tile codes and positioning is bizarrely complicated if you ask me, so I’m not going to explain it. :) Keep searching….

  • http://kenneth.kufluk.com Kenneth

    Line 150 adds the tile into a larger canvas.

    I don’t believe PHP was accepting cookies, so that won’t matter. Google will block your IP, which won’t make you popular at work :)

    Tile codes and positioning is bizarrely complicated if you ask me, so I’m not going to explain it. :) Keep searching….

  • Pingback: links for 2009-02-15 | The Computer Vet Weblog

  • John

    Hi Ken,
    Could you help me to get few tiles from from a mapserver in to a single image
    [http://map.maptelltrack.com/map/tile1.php?map=maptell&key=MP456h6k987hjpk98tyui9jlkmTYUIOP67890appp890sdfgh567tbferd&i=jpeg&t=-348160&l=2396160&s=10000
    ]

    Thanks & Wishing You a Great X Mas

  • Anonymous

    The name’s Kenneth.

  • Agentpastone

    Yea that’s your name!