├── Dockerfile ├── LICENSE ├── README.md ├── requirements.txt └── server.py /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.4 2 | RUN mkdir -p /usr/src/app 3 | WORKDIR /usr/src/app 4 | 5 | VOLUME /mapping 6 | 7 | COPY . /usr/src/app/ 8 | RUN pip install --no-cache-dir -r requirements.txt 9 | 10 | CMD ["python", "-u","/usr/src/app/server.py"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017, KlokanTech.com & OpenMapTiles/OSM2VectorTiles contributors. 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > UPDATE: This utility has been merged to [openmaptiles-tools](https://github.com/openmaptiles/openmaptiles-tools#test-tiles). Look there for updates and new versions... 🚀 2 | 3 | # postserve 4 | Use the ST_AsMVT function to render tiles directly in Postgres 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado==4.4.2 2 | sqlalchemy==1.1.5 3 | mercantile==0.9.0 4 | pyproj==1.9.5.1 5 | pyyaml==3.12 6 | psycopg2==2.6.2 7 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import tornado.ioloop 2 | import tornado.web 3 | import io 4 | import os 5 | 6 | from sqlalchemy import Column, ForeignKey, Integer, String 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.orm import relationship 9 | from sqlalchemy import create_engine 10 | from sqlalchemy import inspect 11 | from sqlalchemy import text 12 | from sqlalchemy.orm import sessionmaker 13 | 14 | import mercantile 15 | import pyproj 16 | import yaml 17 | import sys 18 | import itertools 19 | 20 | def GetTM2Source(file): 21 | with open(file,'r') as stream: 22 | tm2source = yaml.load(stream) 23 | return tm2source 24 | 25 | def GeneratePrepared(layers): 26 | queries = [] 27 | prepared = "PREPARE gettile(geometry, numeric, numeric, numeric) AS " 28 | for layer in layers['Layer']: 29 | layer_query = layer['Datasource']['table'].lstrip().rstrip() # Remove lead and trailing whitespace 30 | layer_query = layer_query[1:len(layer_query)-6] # Remove enough characters to remove first and last () and "AS t" 31 | layer_query = layer_query.replace("geometry", "ST_AsMVTGeom(geometry,!bbox!,4096,0,true) AS mvtgeometry") 32 | 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" 33 | queries.append(base_query.replace("!bbox!","$1").replace("!scale_denominator!","$2").replace("!pixel_width!","$3").replace("!pixel_height!","$4")) 34 | prepared = prepared + " UNION ALL ".join(queries) + ";" 35 | print(prepared) 36 | return(prepared) 37 | 38 | layers = GetTM2Source("/mapping/data.yml") 39 | prepared = GeneratePrepared(layers) 40 | 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')) 41 | inspector = inspect(engine) 42 | DBSession = sessionmaker(bind=engine) 43 | session = DBSession() 44 | session.execute(prepared) 45 | 46 | def bounds(zoom,x,y): 47 | inProj = pyproj.Proj(init='epsg:4326') 48 | outProj = pyproj.Proj(init='epsg:3857') 49 | lnglatbbox = mercantile.bounds(x,y,zoom) 50 | ws = (pyproj.transform(inProj,outProj,lnglatbbox[0],lnglatbbox[1])) 51 | en = (pyproj.transform(inProj,outProj,lnglatbbox[2],lnglatbbox[3])) 52 | return {'w':ws[0],'s':ws[1],'e':en[0],'n':en[1]} 53 | 54 | def zoom_to_scale_denom(zoom): # For !scale_denominator! 55 | # From https://github.com/openstreetmap/mapnik-stylesheets/blob/master/zoom-to-scale.txt 56 | map_width_in_metres = 40075016.68557849 57 | tile_width_in_pixels = 256.0 58 | standardized_pixel_size = 0.00028 59 | map_width_in_pixels = tile_width_in_pixels*(2.0**zoom) 60 | return str(map_width_in_metres/(map_width_in_pixels * standardized_pixel_size)) 61 | 62 | def replace_tokens(query,s,w,n,e,scale_denom): 63 | 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") 64 | 65 | def get_mvt(zoom,x,y): 66 | try: # Sanitize the inputs 67 | sani_zoom,sani_x,sani_y = float(zoom),float(x),float(y) 68 | del zoom,x,y 69 | except: 70 | print('suspicious') 71 | return 1 72 | 73 | scale_denom = zoom_to_scale_denom(sani_zoom) 74 | tilebounds = bounds(sani_zoom,sani_x,sani_y) 75 | s,w,n,e = str(tilebounds['s']),str(tilebounds['w']),str(tilebounds['n']),str(tilebounds['e']) 76 | final_query = "EXECUTE gettile(!bbox!, !scale_denominator!, !pixel_width!, !pixel_height!);" 77 | sent_query = replace_tokens(final_query,s,w,n,e,scale_denom) 78 | response = list(session.execute(sent_query)) 79 | print(sent_query) 80 | layers = filter(None,list(itertools.chain.from_iterable(response))) 81 | final_tile = b'' 82 | for layer in layers: 83 | final_tile = final_tile + io.BytesIO(layer).getvalue() 84 | return final_tile 85 | 86 | class GetTile(tornado.web.RequestHandler): 87 | def get(self, zoom,x,y): 88 | self.set_header("Content-Type", "application/x-protobuf") 89 | self.set_header("Content-Disposition", "attachment") 90 | self.set_header("Access-Control-Allow-Origin", "*") 91 | response = get_mvt(zoom,x,y) 92 | self.write(response) 93 | 94 | def m(): 95 | if __name__ == "__main__": 96 | # Make this prepared statement from the tm2source 97 | application = tornado.web.Application([(r"/tiles/([0-9]+)/([0-9]+)/([0-9]+).pbf", GetTile)]) 98 | print("Postserve started..") 99 | application.listen(8080) 100 | tornado.ioloop.IOLoop.instance().start() 101 | 102 | m() 103 | --------------------------------------------------------------------------------