User:Bettercom/code/mtilegenerator

From OpenStreetMap Wiki
Jump to navigation Jump to search

Please do not use former releases (without VERSION string), they caused a memory leakage in create_tiles().

mtilegenerator.py

#!/usr/bin/env python
# -*- coding: latin-1 -*-

import os, sys
from optparse import OptionParser
from math import pi, cos, sin, log, exp, atan
from subprocess import call
from mapnik import *

VERSION = '0.1.1'

usage = "usage: %prog [options] arg1 arg2"
p = OptionParser(usage=usage)
p.add_option('-m', '--minzoom', dest='minzoom', default=8, type='int', metavar='Z0',
             help='MinZoom at which the renderer should start (default: %default)')
p.add_option('-n', '--maxzoom', dest='maxzoom', default=8, type='int', metavar='Zn', 
             help='MaxZoom at which the renderer should stop (default: %default)')
p.add_option('-t', '--tile', dest='is_tile', default=False, action="store_true",
             help='If set, startx is the Dir of Tile and starty the name of the tilefile')
p.add_option('-a', '--startx', dest='startx', type='float', metavar='X0',
             help="Where to start rendering: X (depends on tile-option!)")
p.add_option('-b', '--starty', dest='starty', type='float', metavar='Y0',
             help="Where to start rendering: Y (depends on tile-option!)")
p.add_option('-x', '--endx', dest='endx', type='float', default=0.0, metavar='Xn',
             help="Where to end rendering: X (depends on tile-option!)")
p.add_option('-y', '--endy', dest='endy', type='float', default=0.0, metavar='Yn',
             help="Where to end rendering: Y (depends on tile-option!)")
p.add_option('-o', '--overwrite', dest='force', default=False, action="store_true",
             help='When set existing tiles will be overwritten, otherwise the renderer will continue without re-rendering')
p.add_option('-f', '--file', dest='file',
             help='Read list of Tiles to [re-] render from file. This implies option --tile.')

DEG_TO_RAD = pi / 180
RAD_TO_DEG = 180 / pi
PRJOPTS    = ' '.join(('+proj=merc',
                       '+a=6378137',
                       '+b=6378137'
                       '+lat_ts=0.0'
                       '+lon_0=0.0'
                       '+x_0=0.0',
                       '+y_0=0',
                       '+k=1.0',
                       '+units=m',
                       '+nadgrids=@null',
                       '+no_defs +over'))

MAPFILE    = os.environ['MAPNIK_MAP_FILE']
TILEDIR    = os.environ['MAPNIK_TILE_DIR']

# The unconverted (larger 32bpp) Water- and Land-Tiles for comparing
WATER = file('%s/WATER0.png' % TILEDIR).read()
LAND  = file('%s/LAND0.png' % TILEDIR).read()

# The converted (smaller 1bpp) Water- and Land-Tile we use for symlinks:
# XXX: Would be better if we could send a 302-redirect instead because
#      client-browser would then cache the URIs of blank tiles
WLINK, LLINK = '../../WATER.png', '../../LAND.png'

# 2009-07-04: We must instantiate these mapnik-classes here on module-
#             level and never in a multible called function (It's
#             faster and otherwise we suffer a mem-leak :-( )
prj = Projection(PRJOPTS)
m = Map(2 * 256, 2 * 256)
load_map(m, MAPFILE)

def minmax (a,b,c):
    a = max(a,b)
    a = min(a,c)
    return a

class GoogleProjection:
    def __init__(self,levels=18):
        self.Bc = []
        self.Cc = []
        self.zc = []
        self.Ac = []
        c = 256
        for d in range(0,levels):
            e = c/2;
            self.Bc.append(c/360.0)
            self.Cc.append(c/(2 * pi))
            self.zc.append((e,e))
            self.Ac.append(c)
            c *= 2
                
    def fromLLtoPixel(self,ll,zoom):
         d = self.zc[zoom]
         e = round(d[0] + ll[0] * self.Bc[zoom])
         f = minmax(sin(DEG_TO_RAD * ll[1]),-0.9999,0.9999)
         g = round(d[1] + 0.5*log((1+f)/(1-f))*-self.Cc[zoom])
         return (e,g)
     
    def fromPixelToLL(self,px,zoom):
         e = self.zc[zoom]
         f = (px[0] - e[0])/self.Bc[zoom]
         g = (px[1] - e[1])/-self.Cc[zoom]
         h = RAD_TO_DEG * ( 2 * atan(exp(g)) - 0.5 * pi)
         return (f,h)

def get_tlist(z=9, maxZoom=13, x=0, y=0):
    """Create a list with tuples of tiles to generate"""
    l = [(z, x, y)]
    for _z in range(z + 1, maxZoom + 1):
        _t = 2 ** (_z - z)
        _s = (x * _t, x * _t + _t, y * _t, y * _t + _t)
        l.extend([(_z, _x, _y) for _x in range(_s[0], _s[1])
                  for _y in range(_s[2], _s[3])])
    return l


def get_tileuri(_t):
    """
    Checks for existing dirs, creates them if necessary and returns
    the full file-name for the tile _t => (z, x, y).
    """
    if not os.path.isdir('%s%s' % (TILEDIR, _t[0])):
        os.mkdir('%s%s' % (TILEDIR, _t[0]))
    if not os.path.isdir('%s%s/%s' % (TILEDIR, _t[0], _t[1])):
        os.mkdir('%s%s/%s' % (TILEDIR, _t[0], _t[1]))
    return '%s%s/%s/%s.png' % (TILEDIR, _t[0], _t[1], _t[2])

def _symlink_tile(_t_uri, _t_type):
    try:
        os.unlink(_t_uri)
    except:
        pass
    if _t_type == 'WATER':
        os.symlink(WLINK, _t_uri)
        return '(symlink to %s) ' % WLINK
    elif _t_type == 'LAND':
        os.symlink(LLINK, _t_uri)
        return '(symlink to %s) ' % LLINK
    return ''

def create_tiles(x=0, y=0, minZoom=8, maxZoom=10, force=False):
    """
    Render Tile z/x/y and all childs up to zoomlevel maxZoom.
    If force is True existing tiles will be overwritten
    """
    gprj = GoogleProjection(maxZoom+1)
    tlist = get_tlist(minZoom, maxZoom, x, y)
    sys.stderr.write('%s tiles to generate, last one is %s\n' % (len(tlist), tlist[-1]))
    while tlist:
        tile = tlist.pop(0)
        tile_uri = get_tileuri(tile)
        if not force and os.path.isfile(tile_uri):
            sys.stderr.write('%s exists, overwriting not forced, loop\n' % tile_uri)
            continue
        elif force and os.path.isfile(tile_uri):
            os.unlink(tile_uri)
        p0 = gprj.fromPixelToLL((tile[1] * 256.0, (tile[2] + 1) * 256.0), tile[0])
        p1 = gprj.fromPixelToLL(((tile[1] + 1) * 256.0, tile[2] * 256.0), tile[0])
        # render a new tile and store it on filesystem
        c0 = prj.forward(Coord(p0[0], p0[1]))
        c1 = prj.forward(Coord(p1[0], p1[1]))
        bbox = Envelope(c0.x, c0.y, c1.x, c1.y)
        bbox.width(bbox.width() * 2)
        bbox.height(bbox.height() * 2)
        m.zoom_to_box(bbox)
        im = Image(512, 512)
        render(m, im)
        view = im.view(128, 128, 256, 256) # x,y,width,height
        f = view.tostring('png')
        if f == WATER:
            _sym = _symlink_tile(tile_uri, 'WATER')
        elif f == LAND:
            _sym = _symlink_tile(tile_uri, 'LAND')
        else:
            view.save(tile_uri, 'png')
            command = "convert  -quiet -colors 255 %s %s" % (tile_uri, tile_uri)
            r = call(command, shell=True)
            _sym = ''
        sys.stderr.write('%s %screated, %s childs remain\n' % (tile_uri, _sym, len(tlist)))

def render_tiles(bbox, minZoom=1, maxZoom=12, force=False):
    sys.stderr.write("render_tiles(%s [%s-%s])\n" %
                     (bbox, minZoom, maxZoom))
    # Create a list of x/y- tiles at min-Zoom-Level and
    # call create_tiles for each
    gprj = GoogleProjection()
    _tl = gprj.fromLLtoPixel((bbox[0], bbox[3]), minZoom)
    _br = gprj.fromLLtoPixel((bbox[2], bbox[1]), minZoom)
    x0, y0 = int(_tl[0] / 256.0), int(_tl[1] / 256.0)
    x1, y1 = int(_br[0] / 256.0), int(_br[1] / 256.0)
    l = [(x, y) for x in range(x0, x1 + 1) for y in range(y0, y1 + 1)
         if x < 2 ** minZoom and y < 2 ** minZoom]
    while l:
        t = l.pop(0)
        sys.stderr.write('Create tiles (and childs) for %s (%s remain)\n' %
                         (t, len(l)))
        create_tiles(t[0], t[1], minZoom, maxZoom, force)

def _main(opts):
    if ((opts.endx and opts.endx < opts.startx) or
        (opts.endy and opts.endy < opts.starty)):
        sys.stderr.write('Error: EndX and/or EndY smaller than StartX/StartY\n')
        sys.exit(1)
    if opts.file:
        try:
            f = file(opts.file)
            l = [l[:-1] for l in f]
            f.close()
            sys.stderr.write('%s tiles have to be [re-] rendered\n' % len(l))
        except Exception, ex:
            sys.stderr.write('Error %s reading file %s\n' % (ex, opts.file))
            sys.exit()
        while l:
            try:
                z, x, y = l.pop(0).split('/')
                z, x, y = int(z), int(x), int(y)
                create_tiles(x, y, z, z, opts.force)
                sys.stderr.write('%s tiles remain for [re-] rendering\n' % len(l))
            except Exception, ex:
                sys.stderr.write('Error %s with line %s\n' % (ex, (z,x,y)))
    elif opts.is_tile and opts.endx and opts.endy:
        l = [(x, y) for x in range(int(opts.startx), int(opts.endx) + 1)
             for y in range(int(opts.starty), int(opts.endy) + 1)]
        while l:
            t = l.pop(0)
            sys.stderr.write('Create tiles (and childs) for %s (%s parents remain) on zoomlevel %s\n' %
                             (t, len(l), opts.minzoom))
            create_tiles(t[0], t[1], opts.minzoom, opts.maxzoom, opts.force)
    elif opts.is_tile:
        print ('Call create_tiles(%i, %i, %i, %i, %s)' %
               (int(opts.startx), int(opts.starty),
                int(opts.minzoom), int(opts.maxzoom),
                bool(opts.force)))
        create_tiles(int(opts.startx), int(opts.starty),
                     int(opts.minzoom), int(opts.maxzoom),
                     bool(opts.force))
    else:
        print ('Call render_tiles(%s, %i, %i, %s)' %
               ((float(opts.startx), float(opts.starty),
                 float(opts.endx), float(opts.endy)),
                int(opts.minzoom), int(opts.maxzoom),
                bool(opts.force)))
        render_tiles((float(opts.startx), float(opts.starty),
                      float(opts.endx), float(opts.endy)),
                     int(opts.minzoom), int(opts.maxzoom),
                     bool(opts.force))

if __name__ == "__main__":
    opts = p.parse_args()[0]
    _main(opts)
    print "mtilegenerator finished"

Fixed version - I hope --Bettercom 18:46, 4 July 2009 (UTC) Another bugfix --Bettercom 07:17, 9 July 2009 (UTC)