diff --git a/Dockerfile b/Dockerfile index 11f1815..860b894 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ FROM python:3.6 -RUN mkdir -p /usr/src/app -WORKDIR /usr/src/app - VOLUME /mapping -COPY . /usr/src/app/ +WORKDIR /usr/src/app + +# Copy requirements.txt first to avoid pip install on every code change +COPY ./requirements.txt /usr/src/app/ RUN pip install --no-cache-dir -r requirements.txt +COPY . /usr/src/app/ + CMD ["python", "-u","/usr/src/app/server.py"] diff --git a/requirements.txt b/requirements.txt index 0d306c5..3b2a7cf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -tornado==4.4.2 -sqlalchemy==1.1.5 -mercantile==0.9.0 -pyproj==1.9.5.1 -psycopg2==2.6.2 +tornado==6.0.1 +sqlalchemy==1.3.1 +psycopg2-binary==2.7.7 diff --git a/server.py b/server.py index 361d9a1..786c175 100644 --- a/server.py +++ b/server.py @@ -1,88 +1,85 @@ +#!/usr/bin/env python +""" +This is a simple vector tile server that returns a PBF tile for /tiles/{z}/{x}/{y}.pbf requests + +Use these environment variables to configure PostgreSQL access: + POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_PASSWORD + +Usage: + server [--fname ] [--port ] + server --help + server --version + + SQL file generated by generate-sqltomvt script with the --prepared flag + +Options: + --fname= Name of the generated function [default: gettile] + -p --port= Serve on this port [default: 8080] + --help Show this screen. + --version Show version. +""" import tornado.ioloop import tornado.web import io import os - +from docopt import docopt from sqlalchemy import create_engine, inspect from sqlalchemy.orm import sessionmaker -import mercantile -import pyproj -import sys -import itertools - - -def getPreparedSql(file): - with open(file, 'r') as stream: - return stream.read() - - -prepared = getPreparedSql("/mapping/mvt/maketile_prep.sql") -engine = create_engine( - 'postgresql://' + os.getenv('POSTGRES_USER', 'openmaptiles') + - ':' + os.getenv('POSTGRES_PASSWORD', 'openmaptiles') + - '@' + os.getenv('POSTGRES_HOST', 'postgres') + - ':' + os.getenv('POSTGRES_PORT', '5432') + - '/' + os.getenv('POSTGRES_DB', 'openmaptiles')) -inspector = inspect(engine) -DBSession = sessionmaker(bind=engine) -session = DBSession() -session.execute(prepared) - - -def bounds(zoom, x, y): - inProj = pyproj.Proj(init='epsg:4326') - outProj = pyproj.Proj(init='epsg:3857') - lnglatbbox = mercantile.bounds(x, y, zoom) - ws = (pyproj.transform(inProj, outProj, lnglatbbox[0], lnglatbbox[1])) - en = (pyproj.transform(inProj, outProj, lnglatbbox[2], lnglatbbox[3])) - return {'w': ws[0], 's': ws[1], 'e': en[0], 'n': en[1]} - - -def replace_tokens(query, s, w, n, e, zoom): - return (query - .replace("!bbox!", "ST_MakeBox2D(ST_Point(" + w + ", " + s + "), ST_Point(" + e + ", " + n + "))") - .replace("!zoom!", zoom) - .replace("!pixel_width!", "256")) - - -def get_mvt(zoom, x, y): - try: - # Sanitize the inputs - sani_zoom, sani_x, sani_y = float(zoom), float(x), float(y) - del zoom, x, y - except: - print('suspicious') - return 1 - - tilebounds = bounds(sani_zoom, sani_x, sani_y) - s, w, n, e = str(tilebounds['s']), str(tilebounds['w']), str(tilebounds['n']), str(tilebounds['e']) - final_query = "EXECUTE gettile(!bbox!, !zoom!, !pixel_width!);" - sent_query = replace_tokens(final_query, s, w, n, e, sani_zoom) - response = list(session.execute(sent_query)) - print(sent_query) - layers = filter(None, list(itertools.chain.from_iterable(response))) - final_tile = b'' - for layer in layers: - final_tile = final_tile + io.BytesIO(layer).getvalue() - return final_tile - class GetTile(tornado.web.RequestHandler): - def get(self, zoom, x, y): - self.set_header("Content-Type", "application/x-protobuf") - self.set_header("Content-Disposition", "attachment") - self.set_header("Access-Control-Allow-Origin", "*") - response = get_mvt(zoom, x, y) - self.write(response) + def initialize(self, session, query): + self.db_session = session + self.db_query = query + + def get(self, z, x, y): + z, x, y = int(z), int(x), int(y) + try: + result = self.db_session.execute(self.db_query, params=dict(z=z, x=x, y=y)).fetchall() + if result: + self.set_header("Content-Type", "application/x-protobuf") + self.set_header("Content-Disposition", "attachment") + self.set_header("Access-Control-Allow-Origin", "*") + value = io.BytesIO(result[0][0]).getvalue() + self.write(value) + print('{0},{1},{2} returned {3} bytes'.format(z, x, y, len(value))) + else: + self.clear() + self.set_status(404) + print('Got NULL result for {0},{1},{2}'.format(z, x, y)) + except Exception as err: + print('{0},{1},{2} threw an exception {3}'.format(z, x, y, err)) + raise + +def main(args): + sqlfile = args[''] + with open(sqlfile, 'r') as stream: + prepared = stream.read() + + pghost = os.getenv('POSTGRES_HOST', 'localhost') + ':' + os.getenv('POSTGRES_PORT', '5432') + pgdb = os.getenv('POSTGRES_DB', 'openmaptiles') + pgcreds = os.getenv('POSTGRES_USER', 'openmaptiles') + ':' + os.getenv('POSTGRES_PASSWORD', 'openmaptiles') + engine = create_engine('postgresql://' + pgcreds + '@' + pghost + '/' + pgdb) + + print('Connecting to PostgreSQL at {0}, db={1}'.format(pghost, pgdb)) + inspector = inspect(engine) + session = sessionmaker(bind=engine)() + session.execute(prepared) + + query = "EXECUTE {0}(:z, :x, :y)".format(args['--fname']) + print('Loaded {0}, will use "{1}" to get vector tiles.'.format(sqlfile, query)) + + port = int(args['--port']) + application = tornado.web.Application([( + r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).pbf", + GetTile, + dict(session=session, query=query) + )]) + application.listen(port) + + print("Postserve started, listening on 0.0.0.0:{0}".format(port)) + tornado.ioloop.IOLoop.instance().start() -def m(): - if __name__ == "__main__": - application = tornado.web.Application([(r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).pbf", GetTile)]) - print("Postserve started..") - application.listen(8080) - tornado.ioloop.IOLoop.instance().start() - - -m() +if __name__ == "__main__": + main(docopt(__doc__, version="1.0"))