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'

\[sourcecode language="php"\]  
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();

?>  
\[/sourcecode\]

Thanks for reading! I guess you could now share this post on TikTok or something. That'd be cool.
Or if you had any comments, you could find me on Threads.

Published