Compare commits

..

6 Commits

Author SHA1 Message Date
Yuri Astrakhan
6e74be938d Documentation and dockerfile cleanups 2019-03-21 20:18:49 -04:00
Yuri Astrakhan
08e3f00920 Dockerfile and requirement fixes 2019-03-21 20:05:29 -04:00
Yuri Astrakhan
1bf8d30e4a Use direct Postgres connection, proper CLI, cleanup
Minimum requirements, proper parameter processing, docs
2019-03-21 19:44:09 -04:00
Yuri Astrakhan
05b142f3ee Rewrote server.py for simplicity, remove sql injection possibility, etc 2019-03-21 17:42:37 -04:00
Yuri Astrakhan
f6468ccc2f fixes 2019-03-21 03:52:25 -04:00
Yuri Astrakhan
c916c911ab Fix ST_ASMVT() signature for current PostGIS 2019-03-19 00:46:33 -04:00
5 changed files with 293 additions and 111 deletions

173
.gitignore vendored Normal file
View File

@ -0,0 +1,173 @@
# Created by .ignore support plugin (hsz.mobi)
### VirtualEnv template
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
.Python
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
.venv
pip-selfcheck.json
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
cmake-build-release/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests

View File

@ -1,10 +1,12 @@
FROM python:3.7
RUN mkdir -p /usr/src/app
FROM python:3.6
LABEL MAINTAINER "Yuri Astrakhan <YuriAstrakhan@gmail.com>"
WORKDIR /usr/src/app
VOLUME /mapping
COPY . /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
CMD ["python", "-u","/usr/src/app/server.py"]
COPY . /usr/src/app/
ENTRYPOINT ["python", "server.py"]

View File

@ -1,2 +1,16 @@
# postserve
Use the ST_AsMVT function to render tiles directly in Postgres
This is an OpenMapTiles map vector tile test server. It requires the prepared SQL statement
generated by the `generate-sqltovmt` tools script.
To run, use this command, replacing `myfile.sql` with the name of the generated file in the current dir.
```
docker run -it --rm --net=host -v "$PWD:/data" openmaptiles/postserve /data/myfile.sql
```
To see help, use
```
docker run -it --rm --net=host -v "$PWD:/data" openmaptiles/postserve --help
```

View File

@ -1,6 +1,3 @@
asyncpg==0.19.0
Click==7.0
mercantile==1.1.2
pyproj==2.4.0
PyYAML==5.1.2
tornado==6.0.3
tornado==6.0.1
psycopg2-binary==2.7.7
docopt==0.6.2

192
server.py
View File

@ -1,108 +1,104 @@
import asyncpg
#!/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 <prepared-sql-file> [--fname <name>] [--port <port>]
server --help
server --version
<prepared-sql-file> SQL file generated by generate-sqltomvt script with the --prepared flag
Options:
--fname=<name> Name of the generated function [default: gettile]
-p --port=<port> Serve on this port [default: 8080]
--help Show this screen.
--version Show version.
"""
import logging
import tornado.ioloop
import tornado.web
import io
import os
from docopt import docopt
import psycopg2
import logging
import tornado
import tornado.web
from tornado.log import enable_pretty_logging
import mercantile
import pyproj
import yaml
import sys
import itertools
log = logging.getLogger('tornado.application')
def GetTM2Source(file):
with open(file,'r') as stream:
tm2source = yaml.load(stream)
return tm2source
def GenerateFunction(layers):
queries = []
function = "CREATE OR REPLACE FUNCTION gettile(geometry, numeric, numeric, numeric) RETURNS SETOF bytea AS $$"
for layer in layers['Layer']:
layer_query = layer['Datasource']['table'].strip()
layer_query = layer_query[1:len(layer_query)-6] # Remove enough characters to remove first and last () and "AS t"
layer_query = layer_query.replace("geometry", "ST_AsMVTGeom(geometry,!bbox!,4096,0,true) AS mvtgeometry")
base_query = "SELECT ST_ASMVT('"+layer['id']+"', 4096, 'mvtgeometry', tile) FROM ("+layer_query+" WHERE ST_AsMVTGeom(geometry, !bbox!,4096,0,true) IS NOT NULL) AS tile"
queries.append(base_query.replace("!bbox!","$1").replace("!scale_denominator!","$2").replace("!pixel_width!","$3").replace("!pixel_height!","$4"))
function = function + " UNION ALL ".join(queries) + ";$$ LANGUAGE SQL"
print(function)
return(function)
dsn = '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')
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 zoom_to_scale_denom(zoom): # For !scale_denominator!
# From https://github.com/openstreetmap/mapnik-stylesheets/blob/master/zoom-to-scale.txt
map_width_in_metres = 40075016.68557849
tile_width_in_pixels = 256.0
standardized_pixel_size = 0.00028
map_width_in_pixels = tile_width_in_pixels*(2.0**zoom)
return str(map_width_in_metres/(map_width_in_pixels * standardized_pixel_size))
def replace_tokens(query,s,w,n,e,scale_denom):
return query.replace("!bbox!","ST_MakeBox2D(ST_Point("+w+", "+s+"), ST_Point("+e+", "+n+"))").replace("!scale_denominator!",scale_denom).replace("!pixel_width!","256").replace("!pixel_height!","256")
async def get_mvt(connection,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
scale_denom = zoom_to_scale_denom(sani_zoom)
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 = "SELECT gettile(!bbox!, !scale_denominator!, !pixel_width!, !pixel_height!);"
sent_query = replace_tokens(final_query,s,w,n,e,scale_denom)
log.info(sent_query)
response = await connection.fetch(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 initialize(self, pool):
self.pool = pool
def initialize(self, fname, connection, query):
self.fname = fname
self.db_connection = connection
self.db_query = query
async 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", "*")
async with self.pool.acquire() as connection:
response = await get_mvt(connection, zoom,x,y)
self.write(response)
def get(self, z, x, y):
z, x, y = int(z), int(x), int(y)
cursor = self.db_connection.cursor()
try:
cursor.execute(self.db_query, (z, x, y))
result = cursor.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},{3}) --> {4:,} bytes'.format(self.fname, z, x, y, len(value)))
else:
self.clear()
self.set_status(404)
print('{0}({1},{2},{3}) is EMPTY'.format(self.fname, z, x, y))
except Exception as err:
print('{0}({1},{2},{3}) threw an exception'.format(self.fname, z, x, y, err))
raise
finally:
cursor.close()
async def get_pool():
pool = await asyncpg.create_pool(dsn = dsn)
layers = GetTM2Source(os.getenv("MAPPING_FILE", "/mapping/data.yml"))
# Make this prepared statement from the tm2source
create_function = GenerateFunction(layers)
async with pool.acquire() as connection:
await connection.execute(create_function)
return pool
def m():
enable_pretty_logging()
io_loop = tornado.ioloop.IOLoop.current()
pool = io_loop.run_sync(get_pool)
application = tornado.web.Application([(r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).pbf", GetTile, dict(pool=pool))])
print("Postserve started..")
application.listen(int(os.getenv("LISTEN_PORT", "8080")))
io_loop.start()
def main(args):
pgdb = os.getenv('POSTGRES_DB', 'openmaptiles')
pghost = os.getenv('POSTGRES_HOST', 'localhost')
pgport = os.getenv('POSTGRES_PORT', '5432')
print('Connecting to PostgreSQL at {0}:{1}, db={2}...'.format(pghost, pgport, pgdb))
connection = psycopg2.connect(
dbname=pgdb,
host=pghost,
port=pgport,
user=os.getenv('POSTGRES_USER', 'openmaptiles'),
password=os.getenv('POSTGRES_PASSWORD', 'openmaptiles'),
)
sqlfile = args['<prepared-sql-file>']
with open(sqlfile, 'r') as stream:
prepared = stream.read()
print('Using prepared SQL:\n\n-------\n\n' + prepared + '\n\n-------\n\n')
cursor = connection.cursor()
try:
cursor.execute(prepared)
finally:
cursor.close()
fname = args['--fname']
query = "EXECUTE {0}(%s, %s, %s)".format(fname)
print('Loaded {0}\nWill use "{1}" to get vector tiles.'.format(sqlfile, query))
tornado.log.access_log.setLevel(logging.ERROR)
port = int(args['--port'])
application = tornado.web.Application([(
r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).pbf",
GetTile,
dict(fname=fname, connection=connection, query=query)
)])
application.listen(port)
print("Postserve started, listening on 0.0.0.0:{0}".format(port))
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
m()
main(docopt(__doc__, version="1.0"))