seed tiles

This commit is contained in:
Shautvast 2026-03-31 21:53:52 +02:00
parent 897aa72cea
commit 9fa252a6af
2 changed files with 136 additions and 0 deletions

View file

@ -31,6 +31,20 @@ services:
- osrm-walking
- osrm-cycling
tile-seeder:
image: python:3-slim
networks:
- maps-net
volumes:
- ./scripts:/scripts:ro
environment:
BACKEND_URL: "http://backend:8080"
SEED_WORKERS: "4"
command: python3 /scripts/seed_tiles.py
restart: "no"
depends_on:
- backend
postgres:
build:
context: .

View file

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Pre-warm the Redis tile cache by fetching all tiles for a bounding box.
Tiles are fetched through the backend, which caches them in Redis automatically.
Run this in the background after starting the stack:
nohup python3 scripts/seed_tiles.py &
Usage:
python3 seed_tiles.py [min_lon] [min_lat] [max_lon] [max_lat] [min_zoom] [max_zoom]
Defaults to the Netherlands at zoom levels 0-12.
"""
import math
import sys
import time
import urllib.request
import urllib.error
from concurrent.futures import ThreadPoolExecutor, as_completed
import os
BACKEND_URL = os.environ.get("BACKEND_URL", "http://localhost:8080")
LAYERS = ["planet_osm_polygon", "planet_osm_line", "planet_osm_point", "planet_osm_roads"]
WORKERS = int(os.environ.get("SEED_WORKERS", "4"))
def wait_for_backend():
"""Wait until the backend health endpoint responds."""
url = f"{BACKEND_URL}/health"
print(f"Waiting for backend at {url}...")
while True:
try:
with urllib.request.urlopen(url, timeout=5) as resp:
if resp.status == 200:
print("Backend is up.")
return
except Exception:
pass
time.sleep(3)
def lon_to_x(lon, zoom):
return int((lon + 180.0) / 360.0 * (1 << zoom))
def lat_to_y(lat, zoom):
lat_r = math.radians(lat)
return int((1.0 - math.log(math.tan(lat_r) + 1.0 / math.cos(lat_r)) / math.pi) / 2.0 * (1 << zoom))
def tiles_for_bbox(min_lon, min_lat, max_lon, max_lat, zoom):
max_coord = (1 << zoom) - 1
x_min = max(0, min(lon_to_x(min_lon, zoom), max_coord))
x_max = max(0, min(lon_to_x(max_lon, zoom), max_coord))
y_min = max(0, min(lat_to_y(max_lat, zoom), max_coord)) # max_lat → smaller y
y_max = max(0, min(lat_to_y(min_lat, zoom), max_coord)) # min_lat → larger y
for x in range(x_min, x_max + 1):
for y in range(y_min, y_max + 1):
yield zoom, x, y
def fetch_tile(layer, z, x, y):
url = f"{BACKEND_URL}/tiles/{layer}/{z}/{x}/{y}.pbf"
try:
with urllib.request.urlopen(url, timeout=120) as resp:
return resp.status == 200
except urllib.error.HTTPError:
return False
except Exception:
return False
def main():
wait_for_backend()
min_lon = float(sys.argv[1]) if len(sys.argv) > 1 else 3.3
min_lat = float(sys.argv[2]) if len(sys.argv) > 2 else 50.7
max_lon = float(sys.argv[3]) if len(sys.argv) > 3 else 7.2
max_lat = float(sys.argv[4]) if len(sys.argv) > 4 else 53.6
min_zoom = int(sys.argv[5]) if len(sys.argv) > 5 else 0
max_zoom = int(sys.argv[6]) if len(sys.argv) > 6 else 12
# Build full task list
tasks = []
for zoom in range(min_zoom, max_zoom + 1):
coords = list(tiles_for_bbox(min_lon, min_lat, max_lon, max_lat, zoom))
tile_count = len(coords)
print(f" z{zoom}: {tile_count} tiles × {len(LAYERS)} layers = {tile_count * len(LAYERS)} requests")
for z, x, y in coords:
for layer in LAYERS:
tasks.append((layer, z, x, y))
total = len(tasks)
print(f"\nTotal: {total} requests — starting with {WORKERS} workers...\n")
done = 0
errors = 0
start = time.time()
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
futures = {executor.submit(fetch_tile, *t): t for t in tasks}
for future in as_completed(futures):
ok = future.result()
done += 1
if not ok:
errors += 1
if done % 50 == 0 or done == total:
elapsed = time.time() - start
rate = done / elapsed if elapsed > 0 else 0
eta = (total - done) / rate if rate > 0 else 0
pct = 100 * done // total
print(f" {done}/{total} ({pct}%) | {errors} errors | {rate:.1f} req/s | ETA {eta:.0f}s")
elapsed = time.time() - start
print(f"\nDone: {total - errors}/{total} tiles seeded in {elapsed:.0f}s")
if __name__ == "__main__":
main()