├── 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 |
--------------------------------------------------------------------------------