Full script: tile.py
Dependencies: Python Image Library and Crazed Monkey’s googletilecutter.sh
Demo: Project CROOS (click “charts” in the top right)

For the Project CROOS Fisherman Portal we needed to create a custom overlay for Google Maps. Follow the link and click “charts” on the top right corner of the map once the map has fully loaded to see the result. To add a custom overlay an image has to be specified for every tile on the map. The javascript api implementation can be found here and here, but that is another topic for another day. This article addresses cutting the image into tiles that and be referenced with the getTileUrl() function.

We started by using a bash script found at Crazed Monkey. Unfortunately this only works at one zoom level so full implementation would require resizing the image in GMIP and repeatedly running the script for every zoom level. Additionally the tile coordinates and padding must be calculated; a very tedious task for more than one zoom level. To get around this I wrote a python script that resizes and recalculates for all levels. The full script can be found here. It is invoked with:

$ python /path/to/script/tile.py /path/to/image/imagename.png N_latitude,W_longitude S_latitude,E_longintude > output.sh
$ bash /path/to/script/output.sh

More succinctly:

$ python ./tile.py <path>/img.png N,W S,E > output.sh
$ ./output.sh

The path to the image MUST be the full path. Note the lack of spaces around the comma separating each component of the coordinate pairs. This is important because the script takes 3 arguments. The output of tile.py is saved to output.sh which executes the tile cutter program.

The script starts out with two functions used to calculate the tile number and pixel number of a given lat/long pair. A more in depth analysis of the coordinate system can be found at Mark McClure’s site out of UNCA. In short Google Maps divides the world into 2^z tiles (where z is the zoom level), by dividing every tile in half at every zoom level. Each tile consists of 256 pixels. These functions convert the corners of the image to Google Maps coordinates.

def latlon2xy(z,lat,lon):
    x,y = latlon2px(z,lat,lon)
    x = int(x/256),int(x%256)
    y = int(y/256),int(y%256)
    return x,y

def latlon2px(z,lat,lon):
    x = 2**z*(lon+180)/360*256
    y = -(.5*math.log((1+math.sin(math.radians(lat)))/(1-math.sin(math.radians(lat))))\
/math.pi-1)*256*2**(z-1)    #hyper complex Mercator projection super trig... blech!
    return x,y

Next the variables to be used are parsed from the command line and processed into working variables:

imageFile = sys.argv[1]
imageName = imageFile.split('/')[-1]
imageDir = '/'.join(imageFile.split('/')[:-1])
latlon1 = eval(sys.argv[2])
latlon2 = eval(sys.argv[3])
img = Image.open(imageFile)
size = img.size
prev = (1,2)
cutter = './googletilecutter-0.11.sh'

The path to crazedmonkey’s cutter script should be set (if it is not in the same directory as the python script). Finally the script iterates over the zoom levels. The values of z should be changed to suit your needs. This for loop resizes the image (so that one image pixel equals one map pixel) and prints out a bash script that will execute the cutter script for the given padding and tile number.

for z in range(5,13):     #zoom levels 5-12
    px1 = latlon2px(z,*latlon1)
    px2 = latlon2px(z,*latlon2)
    width = int(px2[0]-px1[0])
    height = int(px2[1]-px1[1])
    x,y = latlon2xy(z,*latlon1)
    tile = '%s,%s'%(x[0],y[0])
    padding = '%s,%s'%(x[1],y[1])
    try:
        os.mkdir(os.path.join(imageDir,str(z)))
    except:
        pass
    os.chdir(os.path.join(imageDir,str(z)))
    if size[0]*1.5
        img5 = img
    else:
        img5 = img.resize((width, height), Image.NEAREST)
    newImage = os.path.join(os.getcwd(),imageName[:-4]+"-%s"%z+imageName[-4:])
    print img5.size
    img5.save(imageName[:-4]+"-%s"%z+imageName[-4:])
    print 'cd '+os.getcwd()
    print '%s -o %s -t %s -z %s -p %s %s'%(cutter,z,tile,z,padding,newImage)
    prev = tile,padding

The greater than sign (%gt;output.sh) in the first line of the first code block of this article was used to output this script to a file that can then be executed from the command line. At this point you may be wondering “why not just use os.system() or pipe the commands into bash”. Originally the tile cutter script was invoked with os.system(), but memory issues occurred at zoom level 10. By executing the code after the fact, I was able to get up to zoom level 12. The end result will be a folder for every zoom level containing an image for every map tile. For example ./11/z11x321y767.png at z=11, x=321, y=767 (which I believe is the southern Oregon coast).

And now, a few final thoughts on how this could be improved. One weakness in the script is that it requires a continuous image. If two images are used that line up with each other, then there will exist a gap in the overlay where the images meet equal to the “padding” parameter of the tile cutter script. One work around would be to alter the command line to accept multiple files and then use the Python Image Library to correct this phenomenon. An easier work around (but not as modular) would be to make sure that the border on each image lines up with a tile border in Google Maps. At the tile border the padding is 0, and once you correct for zoom level z, the correction will hold true for every zoom level greater than z.

Additionally the script is relying on googletilecutter.sh unnecessarily. We could have taken this one step further and used PIL to cut the map into tiles. This may have allowed us to go to higher zoom levels. However, budget constraints (and a tiny bit of laziness) prevented this. Since there was no obvious advantage to rewriting the script from scratch we stopped short of writing it all in house.

That’s it for now. I’ll be happy to answer any questions.

Full script: tile.py
Demo: Project CROOS (click “charts” in the top left once the map has loaded)

Trackback URI | Comments RSS

Leave a Reply

Please type the characters of this captcha image in the input box

Please type the characters of this captcha image in the input box