#!/usr/bin/env python3 """Generate mbtiles for Martinique by downloading OSM raster tiles. Usage: python3 gen_mbtiles_martinique.py Output: /tmp/mapdata_martinique.mbtiles """ import sqlite3, os, math, time, gzip, io, struct, sys import urllib.request, urllib.error # Martinique bounds (with buffer) MIN_LON, MIN_LAT = -61.7, 13.9 MAX_LON, MAX_LAT = -60.3, 15.1 MIN_Z, MAX_Z = 0, 14 # Number of tiles per zoom level (Marti◆ique is small) # zoom 0: 1, zoom 1: 1, zoom 2: 1, zoom 3: 1, zoom 4: 1 # zoom 5: 1, zoom 6: 2, zoom 7: 4, zoom 8: 6, zoom 9: 12 # zoom 10: 24, zoom 11: 48, zoom 12: 96, zoom 13: 192, zoom 14: 384 TMP_DIR = "/tmp/osm_tiles" OUTPUT = "/tmp/mapdata_martinique.mbtiles" OSM_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png" DELAY = 0.15 # OSM usage policy: max 2req/s UA = "SmartCityMartinique/1.0 (eric@digitribe.fr)" os.makedirs(TMP_DIR, exist_ok=True) def deg2tile(lat, lon, z): n = 1 << z x = int((lon + 180.0) / 360.0 * n) y = int((1.0 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2.0 * n) return max(0, min(n-1, x)), max(0, min(n-1, y)) def tile_exists_in_mbtiles(c, z, x, y_tms): c.execute("SELECT 1 FROM map WHERE zoom_level=? AND tile_column=? AND tile_row=?", (z, x, y_tms)) return c.fetchone() is not None def download_tile(z, x, y): """Download raster tile from OSM. Returns PNG bytes or None.""" url = OSM_URL.format(z=z, x=x, y=y) try: req = urllib.request.Request(url, headers={"User-Agent": UA}) resp = urllib.request.urlopen(req, timeout=10) data = resp.read() if len(data) > 100: return data except urllib.error.HTTPError as e: if e.code == 404: return None print(f" HTTP {e.code} for {z}/{x}/{y}") except Exception as e: print(f" Error {z}/{x}/{y}: {e}") return None def create_mbtiles(): if os.path.exists(OUTPUT): os.remove(OUTPUT) conn = sqlite3.connect(OUTPUT) c = conn.cursor() c.execute("CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT)") c.execute("CREATE TABLE images (tile_data BLOB, tile_id TEXT)") c.execute("CREATE TABLE metadata (name TEXT PRIMARY KEY, value TEXT)") meta = [ ("name", "Martinique"), ("type", "overlay"), ("version", "1.0"), ("description", "OSM raster tiles for Martinique"), ("format", "png"), ("tilejson", "2.1.0"), ("scheme", "tms"), ("minzoom", str(MIN_Z)), ("maxzoom", str(MAX_Z)), ("bounds", f"{MIN_LON},{MIN_LAT},{MAX_LON},{MAX_LAT}"), ("center", "-61.0,14.5,10"), ("attribution", "© OpenStreetMap contributors"), ] c.executemany("INSERT INTO metadata VALUES (?, ?)", meta) return conn, c def main(): conn, c = create_mbtiles() total = 0 downloaded = 0 skipped = 0 start = time.time() for z in range(MIN_Z, MAX_Z + 1): x1, y1 = deg2tile(MIN_LAT, MIN_LON, z) x2, y2 = deg2tile(MAX_LAT, MAX_LON, z) xm, xM = min(x1, x2)-1, max(x1, x2)+1 ym, yM = min(y1, y2)-1, max(y1, y2)+1 xm = max(0, xm) xM = min((1 << z) - 1, xM) ym = max(0, ym) yM = min((1 << z) - 1, yM) z_downloaded = 0 for x in range(xm, xM + 1): for y in range(ym, yM + 1): total += 1 tms_y = (1 << z) - 1 - y png = download_tile(z, x, y) if png: tid = f"osm_{z}_{x}_{tms_y}" c.execute("INSERT INTO images VALUES (?, ?)", (sqlite3.Binary(png), tid)) c.execute("INSERT INTO map VALUES (?, ?, ?, ?)", (z, x, tms_y, tid)) downloaded += 1 z_downloaded += 1 else: skipped += 1 time.sleep(DELAY) conn.commit() print(f"Zoom {z}: {z_downloaded} tiles (total: {downloaded})", flush=True) c.execute("CREATE INDEX idx_map ON map (zoom_level, tile_column, tile_row)") conn.commit() size_mb = os.path.getsize(OUTPUT) / (1024*1024) elapsed = time.time() - start print(f"\nDone! {downloaded}/{total} tiles downloaded, {skipped} empty. {size_mb:.1f} MB in {elapsed:.0f}s") conn.close() if __name__ == "__main__": main()