r.klingeresri-de-esridist

Kitten and Custom Tile Layers in AGOL and ArcGIS Pro

Blog Post created by r.klingeresri-de-esridist Employee on Aug 19, 2018

Sometimes you see this great tile layer in a web map and you want to use it in your ArcGIS online project or ArcGIS Pro. That was the feeling I had, seeing this nice little BVG basemap. The usage of this kind-of-twisted basemap was a bit tricky as the logic for latitude-longitude to tile numbers was a bit twisted. In products like ArcGIS Online/ ArcGIS Enterprise there is no way to alter the logic of this. So if you want to use a tile layer like this in AGOL you need to intervene somewhere else.

Interrupt the Tile Acquisition

The tiles are fetched from a server any time you pan or zoom the map. The approach I used to get the tiles uses a simple php proxy.

So instead of collecting the tiles from the webserver via a direct pull, we use the php script which gets our request, calculates the somewhat twisted tile number for the row and requests the correct tile and forwards it to your AGOL project.

But let us start with a simple trick which serves as the basis: kitten watermarks.

PlaceKitten serves placeholder images in a very common format: http://placekitten.com/256/256?image=5 where the first "256" is the width, the second one the height and the last one the image you would like to see (image number 5). The images are served as PNGs.

placeholder kittenAs said, we will use a php script to fetch the data and send every image back to AGOL as we receive it. This makes no sense at all from a geo perspective. So you will need a php-enabled server. I was using a Windows virtual machine with IIS and installed PHP by using the Web Platform Installer.

So we need a way to fetch images from the web using php. I stumbled upon the cURL method you might already know from the command line world of Linux based systems.

All we need to do is to mimic our script to look like a real computer trying to get an image from this server:

<?php
header ('Content-Type: image/png'); #content returns as an image
$url = 'https://placekitten.com/256/256?image='; #this is the base url
$ynew = rand(1,16); #we can collect one out of 16 images
$url .= $ynew;
$ch = curl_init(); #Initialize a cURL session
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); #don't verify the peer's SSL certificate
curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE); #don't use any cache
curl_setopt($ch,CURLOPT_URL, $url); #provide the URL to use in the request
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); #return the transfer as a string of the return value of curl_exec() instead of outputting it directly.     
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13"); #set HTTP user-agent header
$data = curl_exec($ch); #perform the cURL session
curl_close($ch); #Close the cURL session
echo $data; #return the date (image)
?>

Now you can already use the real URL of your server (don't even try localhost my friend ;-) ) and embed the placeholder cats in your AGOL project.

Open up a new web map and add a new web based layer:

add web layerI am using here the proposed "{level}/{col}/{row}.png" connotation as we would like to create new calls for every image to get a true random pattern:

Look, all those cute kitties...

Running real Cartography

At the very moment we do have an idea on how to forward images from the web into AGOL as a web layer. Now let's move on and fill these placeholder with "real cartography".

As we have seen in this last post, the naming schema for the BVG map is a bit crooked. But first, our script needs to know, which images it should pull. So we need to send the script some parameters. We will do this in a similar way like the real TMS servers: Send the ZXY parameters via the URL, read the in our script and append the URL with them:

<?php
header ('Content-Type: image/png');
$y = $_GET['y']; #the parameter y is stored as a string in the variable y
$x = $_GET['x']; #the parameter x is stor...
$z = $_GET['z']; #you get it, right?
$ynew =( -1*intval($y))+pow(2,intval($z))-1; #recalculate the y coordinate using the found formula.
$url = 'https://fahrinfo.bvg.de/tiles/base/'.$z.'/'.$x.'/'.$ynew.'.png'; #creating the url
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
$data = curl_exec($ch);
curl_close($ch);
echo $data;
?>

The parameters are send to the PHP-enabled server using the following schema:

http://your-server-URL.com/name-of-your-script.php?z={level}&x={col}&y={row}

Using this script you will get a nice BVG basemap in your ArcGIS Online editor Map Viewer:

Once you saved the web map you can even open it in ArcGIS Pro and use it for your purposes by adding the map from Portal:

Multiple Servers

If you want to handle multiple servers with the same script you can add a fourth parameter which handles the tileserver and adding some switches:

<?php
header ('Content-Type: image/png');
$tileservice=null; #
if (isset($_GET['t'])){
     $tileservice = $_GET['t'];
}
if(!$tileservice){
     $tileservice = 'bvg'; #if no tileserver parameter was handled
}
$y = $_GET['y'];
$x = $_GET['x'];
$z = $_GET['z'];
$server = array();
switch ($tileservice){
     case 'cat':
          $url = 'https://placekitten.com/256/256?image=';
          $ynew = rand(1,16);
          $url .= $ynew;
          #echo $url;
          break;
     case 'bvg':
     default:
          $ynew =( -1*intval($y))+pow(2,intval($z))-1;
          $url = 'https://fahrinfo.bvg.de/tiles/base/'.$z.'/'.$x.'/'.$ynew.'.png';
          break;
};    
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
$data = curl_exec($ch);
curl_close($ch);
echo $data;
?>

Now your proxy URL should look like this to get a cat tile:

http://your-server-URL.com/name-of-your-script.php??t=cat&z=15&x=16371&y=10936 

Sometimes you also want to have a fallback server if no tiles were fetched. Therefore we will check response of the cURL call and will use a black-white layer as a fallback layer:

<?php
header ('Content-Type: image/png');
error_reporting(E_ALL);
ini_set('display_errors', '1');
$tileservice=null;
if (isset($_GET['t'])){
     $tileservice = $_GET['t'];
}
if(!$tileservice){
     $tileservice = 'bvg';
}
$y = $_GET['y'];
$x = $_GET['x'];
$z = $_GET['z'];
$server = array();
switch ($tileservice){
     case 'cat':
          #$server[] = 'https://placekitten.com/256/256';
          $url = 'https://placekitten.com/256/256?image=';
          $ynew = rand(1,16);
          $url .= $ynew;
          #echo $url;
          break;
     case 'bvg':
     default:
          $ynew =( -1*intval($y))+pow(2,intval($z))-1;
          $url = 'https://fahrinfo.bvg.de/tiles/base/'.$z.'/'.$x.'/'.$ynew.'.png';
          break;
};    
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
$data = curl_exec($ch);
if (curl_getinfo( $ch, CURLINFO_HTTP_CODE ) != 200) {
     curl_close($ch);
     $server = array();
     $server[] = 'http://a.tiles.wmflabs.org/bw-mapnik/';
     $server[] = 'http://b.tiles.wmflabs.org/bw-mapnik/';
     $server[] = 'http://c.tiles.wmflabs.org/bw-mapnik/';
     $url = $server[array_rand($server)];
     $url .= $z."/".$x."/".$y.".png";
     #echo $url;
     $ch = curl_init();
     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
     curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
     curl_setopt($ch,CURLOPT_URL, $url);
     curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
     curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
     $data = curl_exec($ch);
}
curl_close($ch);
echo $data;
?>

The map shows now both tiles:

If you want to add some "annoying annotation" you might also add a nice watermark on those slippy tiles:

<?php
header ('Content-Type: image/png');
error_reporting(E_ALL);
ini_set('display_errors', '1');
$tileservice=null;
if (isset($_GET['t'])){
     $tileservice = $_GET['t'];
}
if(!$tileservice){
     $tileservice = 'bvg';
}
$y = $_GET['y'];
$x = $_GET['x'];
$z = $_GET['z'];
$server = array();
switch ($tileservice){
     case 'cat':
          #$server[] = 'https://placekitten.com/256/256';
          $url = 'https://placekitten.com/256/256?image=';
          $ynew = rand(1,16);
          $url .= $ynew;
          #echo $url;
          break;
     case 'bvg':
     default:
          $ynew =( -1*intval($y))+pow(2,intval($z))-1;
          $url = 'https://fahrinfo.bvg.de/tiles/base/'.$z.'/'.$x.'/'.$ynew.'.png';
          break;
};    
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
$data = curl_exec($ch);
$size = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD );
if (curl_getinfo( $ch, CURLINFO_HTTP_CODE ) != 200) {
     curl_close($ch);
     $server = array();
     $server[] = 'http://a.tiles.wmflabs.org/bw-mapnik/';
     $server[] = 'http://b.tiles.wmflabs.org/bw-mapnik/';
     $server[] = 'http://c.tiles.wmflabs.org/bw-mapnik/';
     $url = $server[array_rand($server)];
     $url .= $z."/".$x."/".$y.".png";
     #echo $url;
     $ch = curl_init();
     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
     curl_setopt($ch, CURLOPT_FRESH_CONNECT, TRUE);
     curl_setopt($ch,CURLOPT_URL, $url);
     curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
     curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.A.B.C Safari/525.13");
     $data = curl_exec($ch);
}
curl_close($ch);
$im = imagecreatefromstring($data);
$stamp = imagecreatetruecolor(100, 30);
imagestring($stamp, 10, 10, 5, 'watermark', 0xff0000);
// set borders and get sizes
$marge_right = 100;
$marge_bottom = 100;
$sx = imagesx($stamp);
$sy = imagesy($stamp);
// copy the watermark with transparency of 50 into the original image
imagecopymerge($im, $stamp, imagesx($im) - $sx - $marge_right, imagesy($im) - $sy - $marge_bottom, 0, 0, imagesx($stamp), imagesy($stamp), 50);
header('Content-Type: image/png');
imagepng($im);
imagedestroy($im);
?>

But this will create some load on the server... so you might want to rethink this ;-)

Final remarks

Running PHP with a "bad" script can produce a very slow response of your proxied tile server. So you should try to cache tiles on your server. But this is a total different story.

Outcomes