An on-demand tile service for CM93 nautical charts. It indexes a CM93 chart library and serves map tiles for a chart plotter / web map — raster rendered on demand and cached, vector tiles precomputed.
XYZ tiles Web Mercator (EPSG:3857) on-demand + cached precomputed vectors zoom 2–16
Populate the server's /charts by dropping your CM93 zip (e.g.
CM93 2012.zip, ~1.24 GB). It streams to the server, unzips, and
re-indexes — no manual volume mount needed.
/tiles on this origin (no CORS / mixed-content). Only use the separate tiles domain if its scheme matches this page./chartsNeeds a writable /charts volume and free disk for the zip + ~1.6 GB extracted. Serve the page and API over the same scheme (both http or both https) to avoid mixed-content blocks.
Loading…
Drop your BSB chart archives here — they stream to the server, unzip into
/charts-bsb, and are decoded & served as raster tiles at
/bsb/{z}/{x}/{y}.png. You can drop several sets and they
accumulate: Inside Coast.zip, North Coast.zip,
West Coast.zip, etc.
.zip chart sets here/charts-bsbKAP files are decoded directly (RLE + palette, validated pixel-perfect against GDAL) and reprojected to Web Mercator on the fly. First view of a big chart renders in ~1 s, then it's cached.
Loading…
Render & cache every tile for a bounding box across a zoom range, so the map is instant later. Pre-filled for Victoria (SW) → Squamish (N) → Granite Falls, Indian Arm (E).
—
.png) are rendered on demand for whatever tile is requested, then cached to disk — so you never pre-render the whole planet (that's terabytes), only what's viewed..geojson) are compact, so they're precomputed in bulk and also cached; a miss still renders on demand.cm93.cpp) — de-obfuscation, header, vector edges, soundings and feature records → WGS84 geometry, classified via CM93OBJ.DIC → S-57. Renders land, coastline, depth contours/areas, soundings, navaids and hazards./bsb/{z}/{x}/{y}.png.Recommended (same origin, no CORS / mixed-content): the bundled web server reverse-proxies the API at /tiles — it always matches this page's scheme and host:
/tiles e.g. /tiles/raster/7/20/43.png (CM93)
e.g. /tiles/bsb/14/2557/5617.png (BSB/KAP)
The API is also reachable on its own domain, but only use it if its scheme (http/https) matches this page — otherwise the browser blocks it as mixed content:
http://20260530-weather-router-tiles-api.206.116.247.141.sslip.io
FeatureCollection of the chart features intersecting the tile. Precomputed where available, otherwise rendered on demand and cached. Render with Leaflet L.geoJSON / MapLibre.{count, errors, charts:[{name,num,scale,width,height,bbox}]}..zip (e.g. Inside/North/West Coast); unzips into /charts-bsb and re-indexes. Poll /tiles/bsb/upload-status/{job}; history at /tiles/bsb/uploads.kind defaults to all three. Token-guarded if UPLOAD_TOKEN is set. Also available as the Clear tile cache button above.{"ok":true,"root_exists":true}.| Param | Type | Range | Meaning |
|---|---|---|---|
z | int | 2 – 16 | zoom level |
x | int | 0 … 2z−1 | tile column (west→east) |
y | int | 0 … 2z−1 | tile row (north→south, XYZ origin top-left) |
// CM93 raster overlay
L.tileLayer('https://your-domain/tiles/raster/{z}/{x}/{y}.png',
{ maxZoom: 16, tileSize: 256, opacity: 0.95 }).addTo(map);
// BSB / KAP raster overlay
L.tileLayer('https://your-domain/tiles/bsb/{z}/{x}/{y}.png',
{ maxZoom: 16, tileSize: 256, opacity: 1.0 }).addTo(map);
// or vector (CM93)
fetch(`https://your-domain/tiles/vector/${z}/${x}/${y}.geojson`)
.then(r => r.json()).then(fc => L.geoJSON(fc).addTo(map));
In the weather router: Server (advanced) → CM93 chart server URL = /tiles (when served from this stack) and tick Show CM93 charts overlay.
Both endpoints write tiles to a persistent cache directory and serve repeats straight from disk (Cache-Control: public, max-age=86400). Vector tiles can be bulk-warmed with the precompute worker:
python precompute.py --min-zoom 4 --max-zoom 12
Tile rendering is CPU-bound (numpy + Pillow under Python's GIL), so the way to avoid slow tiles / timeouts is:
WORKERS (default min(nproc, 4)) so renders run on multiple cores in parallel. More threads alone can't speed CPU-bound rendering — the GIL serialises it; they only help bursts of already-cached tiles (THREAD_LIMIT, default 64).Compact machine-readable spec for driving this service:
{
"service": "CM93 tile service",
"base": "/tiles",
"scheme": "XYZ (Web Mercator EPSG:3857, y origin top-left)",
"zoom": { "min": 2, "max": 16 },
"endpoints": [
{ "method": "GET", "path": "/raster/{z}/{x}/{y}.png",
"returns": "image/png 256x256",
"behavior": "render-on-demand, disk-cached" },
{ "method": "GET", "path": "/vector/{z}/{x}/{y}.geojson",
"returns": "application/geo+json FeatureCollection",
"behavior": "precomputed-or-on-demand, disk-cached",
"feature_properties": ["kind", "scale", "name"] },
{ "method": "GET", "path": "/bsb/{z}/{x}/{y}.png",
"returns": "image/png 256x256",
"behavior": "BSB/KAP raster charts, render-on-demand, disk-cached" },
{ "method": "GET", "path": "/coverage.json",
"returns": "{root,cells,by_scale,scale_size_deg}" },
{ "method": "GET", "path": "/health",
"returns": "{ok,root_exists}" }
],
"scales": ["Z","A","B","C","D","E","F","G"],
"status": {
"coverage": "decoded (cell bounds per scale)",
"features": "seam: decode_cell_features() (OpenCPN cm93.cpp port pending)"
},
"examples": [
"GET /tiles/raster/7/20/43.png",
"GET /tiles/vector/12/645/1400.geojson",
"GET /tiles/coverage.json"
]
}
Agent tips: tiles are immutable per (z,x,y) for a given chart set; safe to cache aggressively. To find which tiles cover a lat/lon, convert with the standard slippy-map formula. Empty water/land-interior tiles return a small transparent PNG (raster) or an empty features array (vector).
cm93.cpp) and renders the main chart classes. Not yet parsed: attribute values (depth labels, light characteristics) and depth-graduated water shading.