pngtile/wsgi.py
author Tero Marttila <terom@fixme.fi>
Mon, 25 Jan 2010 02:48:41 +0200
changeset 71 01b021e406e9
parent 47 201257cbd887
child 89 02e5b9b08881
permissions -rw-r--r--
pt_cache_size
"""
    Our WSGI web interface, which can serve the JS UI and any .png tiles via HTTP.
"""

from werkzeug import Request, Response, responder
from werkzeug import exceptions
import os.path, os

import pypngtile as pt

DATA_ROOT = os.environ.get("PNGTILE_DATA_PATH") or os.path.abspath('data/')

IMAGE_CACHE = {}

TILE_WIDTH = 256
TILE_HEIGHT = 256

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

def dir_view (req, name, path) :
    prefix = os.path.dirname(req.script_root).rstrip('/')
    script_prefix = req.script_root
    name = name.rstrip('/')


    return """\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>Index of %(dir)s</title>
        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
    </head>
    <body>
        <h1>Index of %(dir)s</h1>

        <ul>
%(listing)s
        </ul>
    </body>
</html>""" % dict(
        prefix          = prefix,
        dir             = name,
        
        listing         = "\n".join(
            """<li><a href="%(url)s">%(name)s</a></li>""" % dict(
                url         = '/'.join((script_prefix, name, item)),
                name        = item,
            ) for item in ['..'] + [i for i in os.listdir(path) if i.endswith('.png') or os.path.isdir(os.path.join(path, i))]
        ),
    )

def image_view (req, image_path, image) :
    image_name = os.path.basename(image_path)

    img_width, img_height = image.info()

    return """\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
    <head>
        <title>%(title)s</title>
        <script src="%(prefix)s/static/prototype.js" type="text/javascript"></script>
        <script src="%(prefix)s/static/dragdrop.js" type="text/javascript"></script>
        <script src="%(prefix)s/static/builder.js" type="text/javascript"></script>
        <script src="%(prefix)s/static/tiles2.js" type="text/javascript"></script>
        <link rel="Stylesheet" type="text/css" href="%(prefix)s/static/style.css">
    </head>
    <body>
        <div id="wrapper">
            <div id="viewport" style="width: 100%%; height: 100%%">
                <div class="overlay">
                    <input type="button" id="btn-zoom-in" value="Zoom In" />
                    <input type="button" id="btn-zoom-out" value="Zoom Out" />
                    <a class="link" id="lnk-image" href="#"></a>
                </div>

                <div class="substrate"></div>

                <div class="background">
                    Loading...
                </div>
            </div>
        </div>

        <script type="text/javascript">
            var tile_source = new Source("%(tile_url)s", %(tile_width)d, %(tile_height)d, -4, 0, %(img_width)d, %(img_height)d);
            var main = new Viewport(tile_source, "viewport");
        </script>
    </body>
</html>""" % dict(
        title           = image_name,
        prefix          = os.path.dirname(req.script_root).rstrip('/'),
        tile_url        = req.url,

        tile_width      = TILE_WIDTH,
        tile_height     = TILE_HEIGHT,

        img_width       = img_width,
        img_height      = img_height,
    )

def scale_by_zoom (val, zoom) :
    if zoom > 0 :
        return val << zoom

    elif zoom > 0 :
        return val >> -zoom

    else :
        return val

def render_tile (image, x, y, zoom, width=TILE_WIDTH, height=TILE_HEIGHT) :
    return image.tile_mem(
        width, height, 
        scale_by_zoom(x, -zoom), scale_by_zoom(y, -zoom), 
        zoom
    )

def render_image (image, cx, cy, zoom, width, height) :
    x = scale_by_zoom(cx - width / 2, -zoom)
    y = scale_by_zoom(cy - height / 2, -zoom)

    # safely limit
    if width * height > MAX_PIXELS :
        raise exceptions.Forbidden("Image too large: %d * %d > %d" % (width, height, MAX_PIXELS))

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

def handle_main (req) :
    # path to image
    image_name = req.path.lstrip('/')
    
    # build absolute path
    image_path = os.path.abspath(os.path.join(DATA_ROOT, image_name))

    
    # ensure the path points inside the data root
    if not image_path.startswith(DATA_ROOT) :
        raise exceptions.NotFound(image_name)


    if os.path.isdir(image_path) :
        return Response(dir_view(req, image_name, image_path), content_type="text/html")
    
    elif not os.path.exists(image_path) :
        raise exceptions.NotFound(image_name)

    elif not image_name or not image_name.endswith('.png') :
        raise exceptions.BadRequest("Not a PNG file")
    

    # get Image object
    if image_path in IMAGE_CACHE :
        # get from cache
        image = IMAGE_CACHE[image_path]

    else :
        # ensure exists
        if not os.path.exists(image_path) :
            raise exceptions.NotFound(image_name)

        # cache
        image = IMAGE_CACHE[image_path] = pt.Image(image_path)
    
    if image.status() == pt.CACHE_NONE :
        raise exceptions.InternalServerError("Image not cached: " + image_name)

    # what view?
    if not req.args :
        # viewport
        return Response(image_view(req, image_path, image), content_type="text/html")

    elif 'w' in req.args and 'h' in req.args and 'cx' in req.args and 'cy' in req.args :
        # specific image
        width = int(req.args['w'])
        height = int(req.args['h'])
        cx = int(req.args['cx'])
        cy = int(req.args['cy'])
        zoom = int(req.args.get('zl', "0"))

        # yay full render
        return Response(render_image(image, cx, cy, zoom, width, height), content_type="image/png")

    elif 'x' in req.args and 'y' in req.args :
        # tile
        x = int(req.args['x'])
        y = int(req.args['y'])
        zoom = int(req.args.get('zl', "0"))
        
        # yay render
        return Response(render_tile(image, x, y, zoom), content_type="image/png")
   
    else :
        raise exceptions.BadRequest("Unknown args")
   

@responder
def application (env, start_response) :
    req = Request(env, start_response)
    
    try :
        return handle_main(req)

    except exceptions.HTTPException, e :
        return e