├── requirements.txt ├── robots.txt ├── .gitignore ├── favicon.ico ├── conf ├── servembtiles-uwsgi.conf ├── servembtiles_uwsgi.ini ├── servembtiles_nginx.conf └── servembtiles_cache_nginx.conf ├── settings.py ├── index.html ├── LICENSE.md ├── README.md └── servembtiles.py /requirements.txt: -------------------------------------------------------------------------------- 1 | uwsgi 2 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | mapdata/* 2 | servembtiles.sock 3 | __pycache__/* 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkut/servembtiles/HEAD/favicon.ico -------------------------------------------------------------------------------- /conf/servembtiles-uwsgi.conf: -------------------------------------------------------------------------------- 1 | # uWSGI - Manage uWSGI Application Server 2 | description "servembtiles (mbtiles Map Tile Server) uWSGI Application" 3 | start on (filesystem and net-device-up IFACE=lo) 4 | stop on runlevel [!2345] 5 | respawn 6 | exec /var/www/servembtiles/bin/uwsgi /var/www/servembtiles/repo/servembtiles_uwsgi.ini 7 | 8 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | MBTILES_ABSPATH = '/var/www/servembtiles/repo/mapdata/OSMBrightGray.mbtiles' 2 | MBTILES_TILE_EXT = '.png' # VALID Extentions: (".png", ".jpg", ".jpeg") 3 | MBTILES_ZOOM_OFFSET = 0 4 | MBTILES_HOST = 'localhost' 5 | MBTILES_PORT = 8005 6 | MBTILES_SERVE = False 7 | USE_OSGEO_TMS_TILE_ADDRESSING = True # False will use google XYZ addressing 8 | 9 | -------------------------------------------------------------------------------- /conf/servembtiles_uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | master = true 3 | processes = 3 4 | socket = /var/www/servembtiles/repo/servembtiles.sock 5 | chmod-socket = 664 6 | chown-socket = www-data:www-data 7 | uid = www-data 8 | gid = www-data 9 | vacuum = true 10 | 11 | chdir = /var/www/servembtiles/repo/ 12 | #env = DJANGO_SETTINGS_MODULE=valet.settings 13 | module = servembtiles 14 | virtualenv = /var/www/servembtiles 15 | logfile-chmod = 666 16 | logto = /var/log/uwsgi/servembtiles-uwsgi.log 17 | -------------------------------------------------------------------------------- /conf/servembtiles_nginx.conf: -------------------------------------------------------------------------------- 1 | # servembtiles_nginx.conf 2 | 3 | # upstream component nginx needs to connect to 4 | upstream servembtiles{ 5 | server unix:///var/www/servembtiles/repo/servembtiles.sock; 6 | } 7 | 8 | # configuration of user-facing web-server 9 | server { 10 | listen 8005; 11 | server_name servembtiles; 12 | access_log /var/log/nginx/servembtiles-access.log; 13 | error_log /var/log/nginx/servembtiles-error.log error; 14 | charset utf-8; 15 | 16 | # max upload size 17 | client_max_body_size 75M; 18 | 19 | # test page 20 | location /index.html { 21 | alias /var/www/servembtiles/repo/index.html; 22 | } 23 | 24 | # base site 25 | location / { 26 | uwsgi_pass servembtiles; # name defined in 'upstream' config 27 | include /etc/nginx/uwsgi_params; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | servembtiles.py test map 6 | 7 | 8 | 9 | 10 |
11 | 12 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Shane Cousins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /conf/servembtiles_cache_nginx.conf: -------------------------------------------------------------------------------- 1 | # http://wiki.nginx.org/QuickStart 2 | # http://wiki.nginx.org/Configuration 3 | # 4 | # Generally, you will want to move this file somewhere, and start with a clean 5 | # file but keep this around for reference. Or just disable in sites-enabled. 6 | # 7 | # Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. 8 | ## 9 | proxy_cache_path /tmp/nginx levels=1:2 keys_zone=servembtiles_cache_zone:120m inactive=160m; 10 | 11 | server { 12 | listen 80 default_server; 13 | listen [::]:80 default_server ipv6only=on; 14 | access_log /var/log/nginx/servembtiles-cache-access.log; 15 | error_log /var/log/nginx/servembtiles-cache-error.log error; 16 | 17 | server_name servembtiles_cache; 18 | proxy_cache_key "$scheme$request_method$host$request_uri"; 19 | 20 | location / { 21 | proxy_cache servembtiles_cache_zone; 22 | #proxy_cache_bypass $http_cache_control; 23 | add_header X-Proxy-Cache $upstream_cache_status; 24 | 25 | # servembtils.py port (configured in servembtiles_nginx.conf) 26 | proxy_pass http://localhost:8005; 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # servembtiles (MBTiles Tile Map Server) 2 | 3 | `servembtiles.py` is a pure python3 wsgi application for serving [MBTiles](https://github.com/mapbox/mbtiles-spec). 4 | MBTiles can be exported from [Tilemill](https://www.mapbox.com/tilemill/). 5 | 6 | The application's MBTiles filepath can be defined in the 'settings.py' variable, *MBTILES_ABSPATH*. 7 | The application's tile image file extension can also be set via the 'settings.py' variable, *MBTILES_TILE_EXT*. 8 | 9 | By default requests are expected to follow the TMS addressing scheme. (For example, '/z/x/y.png') 10 | See (http://www.maptiler.org/google-maps-coordinates-tile-bounds-projection/) for details. 11 | The Google maps XYZ scheme is supported by setting the *USE_OSGEO_TMS_TILE_ADDRESSING* value to *False* in the 'settings.py' file. 12 | 13 | In addition, the `/metadata/` URL is available to view the .mbtiles file's metadata table in `json` format. 14 | (And if the included sample configurations and `index.html` are used, `/index.html` provides a test map to confirm that your tiles are being served from the defined `.mbtiles` file) 15 | 16 | This includes a simple test server for verification purposes. 17 | The Installation section below describes how to configure this to serve tiles with reverse-proxy caching using Nginx, 18 | with the configuration files included in this project. 19 | 20 | ## Test Server Usage 21 | 22 | The test server is implemented through `from wsgiref.simple_server import make_server`. 23 | 24 | ``` 25 | $ python3 servembtiles.py --serve --filepath exports/OSMBright.mbtiles 26 | 2015-02-24 16:27:45,473 - servembtiles - INFO - FILEPATH: /home/myuser/mapdata/exports/OSMBright.mbtiles 27 | 2015-02-24 16:27:45,473 - servembtiles - INFO - TILE EXT: .png 28 | 2015-02-24 16:27:45,474 - servembtiles - INFO - ADDRESS : localhost 29 | 2015-02-24 16:27:45,474 - servembtiles - INFO - PORT : 8005 30 | ``` 31 | 32 | 33 | ## Requirements 34 | 35 | - Python 3.5.X 36 | 37 | ## settings.py 38 | 39 | The 'settings.py' file contains the following values: 40 | 41 | * MBTILES_ABSPATH - The absolute path to the mbtiles file to serve 42 | 43 | * MBTILES_TILE_EXT - The image extension to use (".png", ".jpg", ".jpeg") 44 | 45 | * MBTILES_ZOOM_OFFSET - Apply a integer zoom offset, default '0' 46 | 47 | * USE_OSGEO_TMS_TILE_ADDRESSING - True (default) set to False to use Google XYZ addressing. 48 | 49 | ## Installation 50 | 51 | This section describes the initial application installation method on ubuntu 14.04. 52 | 53 | 1. Create 'venv' on target server: 54 | 55 | ```console 56 | $ cd /var/www 57 | $ sudo mkdir servembtiles 58 | $ sudo chmod 777 servembtiles 59 | $ python3 -m venv servembtiles 60 | ``` 61 | 62 | 2. Clone 'servembtiles' repository: 63 | 64 | ```console 65 | $ cd /var/www/servembtiles 66 | $ git clone GIT_REPOSTIORY_URL repo 67 | ``` 68 | 69 | 3. Activate *venv* and install requirements: 70 | 71 | ```console 72 | $ source bin/activate 73 | (servembtiles)$ cd repo 74 | (servembtiles)$ pip install -r requirements.txt 75 | ``` 76 | 77 | 4. Symlink the 'servembtiles_nginx.conf' to */etc/nginx/sites-enabled/* 78 | 79 | ```console 80 | $ sudo ln -s /var/www/servembtiles/repo/conf/servembtiles_nginx.conf /etc/nginx/sites-enabled/ 81 | $ sudo ln -s /var/www/servembtiles/repo/conf/servembtiles_cache_nginx.conf /etc/nginx/sites-enabled/ 82 | ``` 83 | 84 | 85 | 5. To allow the socket (for uwsgi<->nginx comunication) to be created, chmod on servembtiles/repo directory: 86 | 87 | ```console 88 | sudo chmod 777 /var/www/servembtiles/repo 89 | ``` 90 | 91 | 6. Copy upstart configuration file, *servembtiles-uwsgi.conf* to /etc/init to run application as a service: 92 | 93 | ```console 94 | sudo cp /var/www/servembtiles/repo/servembtiles-uwsgi.conf /etc/init 95 | ``` 96 | 97 | 7. Copy Map Data (or configure settings.py to a location which the nginx user can access) 98 | 99 | ```console 100 | $ mkdir /var/www/servembtiles/servembtiles/mapdata 101 | $ cp [MAPDATA.mbtiles] /var/www/servembtiles/repo/mapdata 102 | ``` 103 | 104 | 8. Update host addres in *index.html* 105 | 106 | ``` 107 | # Update only if you plan to use index.html included 108 | ... 109 | 'http://localhost:8005/{z}/{x}/{y}.png', { // <-- localhost must be changed to browser accessible host 110 | ... 111 | ``` 112 | 113 | 9. Start uwsgi application & Restart nginx 114 | 115 | ```console 116 | sudo service servembtiles-uwsgi start 117 | sudo service nginx restart 118 | ``` 119 | 120 | > Note: Related log files are located in "/var/log/nginx" & "/var/log/uwsgi/servembtiles-uwsgi.log" 121 | > Application configured to be served at SERVER_IP_ADDRESS:8005 122 | > *Port configured in servembtiles_nginx.conf file* 123 | 124 | 125 | -------------------------------------------------------------------------------- /servembtiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | mbtiles WSGI application 4 | 5 | MBTiles is a specification for storing tiled map data in SQLite databases for immediate usage and for transfer. 6 | From: 7 | https://github.com/mapbox/mbtiles-spec 8 | """ 9 | import os 10 | import json 11 | import sqlite3 12 | import mimetypes 13 | import logging 14 | from wsgiref.util import shift_path_info 15 | 16 | 17 | logger = logging.getLogger(__name__) 18 | logger.setLevel(logging.WARNING) 19 | 20 | try: 21 | from settings import MBTILES_ABSPATH, MBTILES_TILE_EXT, MBTILES_ZOOM_OFFSET, MBTILES_HOST, MBTILES_PORT, MBTILES_SERVE, USE_OSGEO_TMS_TILE_ADDRESSING 22 | except ImportError: 23 | logger.warn("settings.py not set, may not be able to run via a web server (apache, nginx, etc)!") 24 | MBTILES_ABSPATH = None 25 | MBTILES_TILE_EXT = '.png' 26 | MBTILES_ZOOM_OFFSET = 0 27 | MBTILES_HOST = 'localhost' 28 | MBTILES_PORT = 8005 29 | MBTILES_SERVE = False 30 | USE_OSGEO_TMS_TILE_ADDRESSING = True 31 | 32 | SUPPORTED_IMAGE_EXTENSIONS = (".png", ".jpg", ".jpeg") 33 | 34 | 35 | class MBTilesFileNotFound(Exception): 36 | pass 37 | 38 | 39 | class InvalidImageExtension(Exception): 40 | pass 41 | 42 | 43 | class MBTilesApplication: 44 | """ 45 | Serves rendered tiles within the given .mbtiles (sqlite3) file defined in settings.MBTILES_ABSPATH 46 | 47 | Refer to the MBTiles specification at: 48 | https://github.com/mapbox/mbtiles-spec 49 | """ 50 | 51 | def __init__(self, mbtiles_filepath, tile_image_ext='.png', zoom_offset=0): 52 | if mbtiles_filepath is None or not os.path.exists(mbtiles_filepath): 53 | raise MBTilesFileNotFound(mbtiles_filepath) 54 | 55 | if tile_image_ext not in SUPPORTED_IMAGE_EXTENSIONS: 56 | raise InvalidImageExtension("{} not in {}!".format(tile_image_ext, SUPPORTED_IMAGE_EXTENSIONS)) 57 | 58 | self.mbtiles_db = sqlite3.connect( 59 | "file:{}?mode=ro".format(mbtiles_filepath), 60 | check_same_thread=False, uri=True) 61 | self.tile_image_ext = tile_image_ext 62 | self.tile_content_type = mimetypes.types_map[tile_image_ext.lower()] 63 | self.zoom_offset = zoom_offset 64 | self.maxzoom = None 65 | self.minzoom = None 66 | 67 | self._populate_supported_zoom_levels() 68 | 69 | def _populate_supported_zoom_levels(self): 70 | """ 71 | Query the metadata table and obtain max/min zoom levels, 72 | setting to self.minzoom, self.maxzoom as integers 73 | :return: None 74 | """ 75 | query = 'SELECT name, value FROM metadata WHERE name="minzoom" OR name="maxzoom";' 76 | # add maxzoom, minzoom to instance 77 | for name, value in self.mbtiles_db.execute(query): 78 | setattr(self, name.lower(), max(int(value) - self.zoom_offset, 0)) 79 | 80 | def __call__(self, environ, start_response): 81 | if environ['REQUEST_METHOD'] == 'GET': 82 | uri_field_count = len(environ['PATH_INFO'].split('/')) 83 | base_uri = shift_path_info(environ) 84 | 85 | # handle 'metadata' requests 86 | if base_uri == 'metadata': 87 | query = 'SELECT * FROM metadata;' 88 | metadata_results = self.mbtiles_db.execute(query).fetchall() 89 | if metadata_results: 90 | status = '200 OK' 91 | response_headers = [('Content-type', 'application/json')] 92 | start_response(status, response_headers) 93 | json_result = json.dumps(metadata_results, ensure_ascii=False) 94 | return [json_result.encode("utf8"),] 95 | else: 96 | status = '404 NOT FOUND' 97 | response_headers = [('Content-type', 'text/plain; charset=utf-8')] 98 | start_response(status, response_headers) 99 | return ['"metadata" not found in configured .mbtiles file!'.encode('utf8'), ] 100 | 101 | # handle tile request 102 | elif uri_field_count >= 3: # expect: zoom, x & y 103 | try: 104 | zoom = int(base_uri) 105 | if None not in (self.minzoom, self.maxzoom) and not (self.minzoom <= zoom <= self.maxzoom): 106 | status = "404 Not Found" 107 | response_headers = [('Content-type', 'text/plain; charset=utf-8')] 108 | start_response(status, response_headers) 109 | return ['Requested zoomlevel({}) Not Available! Valid range minzoom({}) maxzoom({}) PATH_INFO: {}'.format(zoom, 110 | self.minzoom, 111 | self.maxzoom, 112 | environ['PATH_INFO']).encode('utf8')] 113 | zoom += self.zoom_offset 114 | x = int(shift_path_info(environ)) 115 | y, ext = shift_path_info(environ).split('.') 116 | y = int(y) 117 | except ValueError as e: 118 | status = "400 Bad Request" 119 | response_headers = [('Content-type', 'text/plain; charset=utf-8')] 120 | start_response(status, response_headers) 121 | return ['Unable to parse PATH_INFO({}), expecting "z/x/y.(png|jpg)"'.format(environ['PATH_INFO']).encode('utf8'), ' '.join(i for i in e.args).encode('utf8')] 122 | 123 | query = 'SELECT tile_data FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?;' 124 | if not USE_OSGEO_TMS_TILE_ADDRESSING: 125 | # adjust y to use XYZ google addressing 126 | ymax = 1 << zoom 127 | y = ymax - y - 1 128 | values = (zoom, x, y) 129 | tile_results = self.mbtiles_db.execute(query, values).fetchone() 130 | 131 | if tile_results: 132 | tile_result = tile_results[0] 133 | status = '200 OK' 134 | response_headers = [('Content-type', self.tile_content_type)] 135 | start_response(status, response_headers) 136 | return [tile_result,] 137 | else: 138 | status = '404 NOT FOUND' 139 | response_headers = [('Content-type', 'text/plain; charset=utf-8')] 140 | start_response(status, response_headers) 141 | return ['No data found for request location: {}'.format(environ['PATH_INFO']).encode('utf8')] 142 | 143 | status = "400 Bad Request" 144 | response_headers = [('Content-type', 'text/plain; charset=utf-8')] 145 | start_response(status, response_headers) 146 | return ['request URI not in expected: ("metadata", "/z/x/y.png")'.encode('utf8'), ] 147 | 148 | 149 | if __name__ == '__main__': 150 | import argparse 151 | parser = argparse.ArgumentParser() 152 | parser.add_argument("--serve", 153 | default=MBTILES_SERVE, 154 | action='store_true', 155 | help="Start test server[DEFAULT={}]\n(Defaults to enviornment variable, 'MBTILES_SERVE')".format(MBTILES_SERVE)) 156 | parser.add_argument('-p', '--port', 157 | default=MBTILES_PORT, 158 | type=int, 159 | help="Test server port [DEFAULT={}]\n(Defaults to enviornment variable, 'MBTILES_PORT')".format(MBTILES_PORT)) 160 | parser.add_argument('-a', '--address', 161 | default=MBTILES_HOST, 162 | help="Test address to serve on [DEFAULT=\"{}\"]\n(Defaults to enviornment variable, 'MBTILES_HOST')".format(MBTILES_HOST)) 163 | parser.add_argument('-f', '--filepath', 164 | default=MBTILES_ABSPATH, 165 | help="mbtiles filepath [DEFAULT={}]\n(Defaults to enviornment variable, 'MBTILES_ABSFILEPATH')".format(MBTILES_ABSPATH)) 166 | parser.add_argument('-e', '--ext', 167 | default=MBTILES_TILE_EXT, 168 | help="mbtiles image file extention [DEFAULT={}]\n(Defaults to enviornment variable, 'MBTILES_TILE_EXT')".format(MBTILES_TILE_EXT)) 169 | parser.add_argument('-z', '--zoom-offset', 170 | default=MBTILES_ZOOM_OFFSET, 171 | type=int, 172 | help="mbtiles zoom offset [DEFAULT={}]\n(Defaults to enviornment variable, 'MBTILES_ZOOM_OFFSET')".format(MBTILES_ZOOM_OFFSET)) 173 | args = parser.parse_args() 174 | args.filepath = os.path.abspath(args.filepath) 175 | if args.serve: 176 | # create console handler and set level to debug 177 | console = logging.StreamHandler() 178 | console.setLevel(logging.DEBUG) 179 | formatter = logging.Formatter('%(asctime)s - %(module)s - %(levelname)s - %(message)s') 180 | console.setFormatter(formatter) 181 | logger.addHandler(console) 182 | logger.setLevel(logging.DEBUG) 183 | 184 | logger.info("FILEPATH: {}".format(args.filepath)) 185 | logger.info("TILE EXT: {}".format(args.ext)) 186 | logger.info("ADDRESS : {}".format(args.address)) 187 | logger.info("PORT : {}".format(args.port)) 188 | 189 | from wsgiref.simple_server import make_server, WSGIServer 190 | from socketserver import ThreadingMixIn 191 | class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): pass 192 | 193 | mbtiles_app = MBTilesApplication(mbtiles_filepath=args.filepath, tile_image_ext=args.ext, zoom_offset=args.zoom_offset) 194 | server = make_server(args.address, args.port, mbtiles_app, ThreadingWSGIServer) 195 | try: 196 | server.serve_forever() 197 | except KeyboardInterrupt: 198 | logger.info("stopped.") 199 | else: 200 | logger.warn("'--serve' option not given!") 201 | logger.warn("\tRun with the '--serve' option to serve tiles with the test server.") 202 | else: 203 | application = MBTilesApplication(mbtiles_filepath=MBTILES_ABSPATH, tile_image_ext=MBTILES_TILE_EXT, zoom_offset=MBTILES_ZOOM_OFFSET) 204 | --------------------------------------------------------------------------------