pngtile/tile.py
author Tero Marttila <terom@qmsk.net>
Sat, 04 Oct 2014 03:28:57 +0300
changeset 175 16ab69a5569a
parent 174 58c9e2de0dd4
child 182 cc6c6e7fd39a
permissions -rw-r--r--
pngtile: fix multiple bugs with region-link centering
"""
    Raw tile handling.
"""

from werkzeug import Request, Response, exceptions
from werkzeug.utils import redirect
import werkzeug.urls

import pngtile.application
import pypngtile

import datetime

## Coordinates
# width of a tile
TILE_SIZE = 256

# maximum zoom out
MAX_ZOOM = 4

# max. output resolution to allow
MAX_PIXELS = 1920 * 1200

def scale (val, zoom):
    """
        Scale dimension by zoom factor

        zl > 0 -> bigger
        zl < 0 -> smaller
    """

    if zoom > 0:
        return val << zoom

    elif zoom < 0:
        return val >> -zoom

    else:
        return val

def scale_center (val, dim, zoom):
    """
        Scale value about center by zoom.
    """

    return scale(val - dim / 2, zoom)

class TileApplication (pngtile.application.PNGTileApplication):
    # age in seconds for caching an unknown-mtime image for revalidates
    MIN_AGE = 10 # 10 seconds

    # age in seconds for caching a known-mtime image
    MAX_AGE = 7 * 24 * 60 * 60 # 1 week

    def __init__ (self, image_server, **opts):
        """
            image_server:       http://.../ url to image-server frontend
        """

        super(TileApplication, self).__init__(**opts)

        self.image_server = image_server
        
    def image_url (self, name):
        return werkzeug.urls.Href(self.image_server)(path)

    def render_region (self, request, image):
        """
            Handle request for an image region
        """

        width = int(request.args['w'])
        height = int(request.args['h'])
        x = int(request.args['x'])
        y = int(request.args['y'])
        zoom = int(request.args.get('zoom', "0"))

        # safety limit
        if width * height > MAX_PIXELS:
            raise exceptions.BadRequest("Image size: %d * %d > %d" % (width, height, MAX_PIXELS))

        if zoom > MAX_ZOOM:
            raise exceptions.BadRequest("Image zoom: %d > %d" % (zoom, MAX_ZOOM))

        x = scale_center(x, width, zoom)
        y = scale_center(y, height, zoom)
        
        try:
            return image.tile_mem(width, height, x, y, zoom)

        except pypngtile.Error as error:
            raise exceptions.InternalServerError(str(error))
        
    def render_tile (self, request, image):
        """
            Handle request for image tile
        """

        width = TILE_SIZE
        height = TILE_SIZE
        x = int(request.args['x'])
        y = int(request.args['y'])
        zoom = int(request.args.get('zoom', "0"))
        
        if zoom > MAX_ZOOM:
            raise exceptions.BadRequest("Image zoom: %d > %d" % (zoom, MAX_ZOOM))

        x = scale(x * width, zoom)
        y = scale(y * height, zoom)

        try:
            return image.tile_mem(width, height, x, y, zoom)

        except pypngtile.Error as error:
            raise exceptions.InternalServerError(str(error))

    def handle_dir (self, request, name, path):
        """
            Redirect to the image frontend for a non-tile request.
        """

        if not name.endswith('/'):
            # avoid an additional redirect
            name += '/'

        return redirect(self.image_url(name))

    def handle_image (self, request, name, path):
        """
            Redirect to the image frontend for a non-tile request.
        """

        return redirect(self.image_url(name))

    def handle (self, request):
        """
            Handle request for an image
        """
        
        name, path, type = self.lookup(request.path)
        
        # determine handler
        if not type:
            return self.handle_dir(request, name, path)

        elif not request.args:
            return self.handle_image(request, name, path)

        elif 'w' in request.args and 'h' in request.args and 'x' in request.args and 'y' in request.args:
            render_func = self.render_region

        elif 'x' in request.args and 'y' in request.args:
            render_func = self.render_tile

        else:
            raise exceptions.BadRequest("Unknown args")
        
        # handle image
        image, name = self.open(request.path)

        # http caching
        mtime = image.cache_mtime()

        if 't' in request.args:
            try:
                ttime = datetime.datetime.utcfromtimestamp(int(request.args['t']))
            except ValueError:
                ttime = None
        else:
            ttime = None

        if request.if_modified_since and mtime == request.if_modified_since:
            response = Response(status=304)
        else:
            # render 
            png = render_func(request, image)
        
            response = Response(png, content_type='image/png')
        
        # cache out
        response.last_modified = mtime

        if not ttime:
            # cached item may change while url stays the same
            response.headers['Cache-Control'] = 'max-age={min_age:d}'.format(min_age=self.MIN_AGE)

        elif ttime == mtime:
            # url will change if content changes
            response.headers['Cache-Control'] = 'max-age={max_age:d}'.format(max_age=self.MAX_AGE)

        else:
            # XXX: mismatch wtf
            response.headers['Cache-Control'] = 'must-revalidate'

        return response