← Ocean Routing

🗺️ CM93 Chart Tiling API

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

Upload charts

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.

Defaults to /tiles on this origin (no CORS / mixed-content). Only use the separate tiles domain if its scheme matches this page.
⬆️
Drag & drop your CM93 .zip here
or click to choose · streams & unzips into /charts

Needs 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.

Uploaded files

Loading…

Upload BSB / KAP raster charts

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.

🌊
Drag & drop your BSB .zip chart sets here
or click to choose · multiple files OK · streams & unzips into /charts-bsb

KAP 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.

Uploaded BSB sets

Loading…

Precompute an area

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).

Tile access log

What it does

Base URL

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

Endpoints

GET/tiles/raster/{z}/{x}/{y}.png
256×256 PNG XYZ tile, Web-Mercator, transparent background. Rendered on demand and cached. Use directly as a Leaflet / MapLibre / OpenLayers tile layer.
GET/tiles/vector/{z}/{x}/{y}.geojson
A GeoJSON FeatureCollection of the chart features intersecting the tile. Precomputed where available, otherwise rendered on demand and cached. Render with Leaflet L.geoJSON / MapLibre.
GET/tiles/bsb/{z}/{x}/{y}.png
256×256 PNG XYZ tile rendered from the uploaded BSB / KAP raster charts, Web-Mercator, transparent outside chart coverage. Rendered on demand and cached. Upload chart sets with the BSB drop-zone above.
GET/tiles/bsb/coverage.json
Indexed BSB charts: {count, errors, charts:[{name,num,scale,width,height,bbox}]}.
POST/tiles/bsb/upload
Stream a BSB .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.
GET/tiles/coverage.json
CM93 library summary: total cells, count per scale, and the cell size (degrees) per scale.
POST/tiles/cache/clear?kind=raster,vector,bsb
Delete cached tiles so they re-render with the current renderer. kind defaults to all three. Token-guarded if UPLOAD_TOKEN is set. Also available as the Clear tile cache button above.
GET/tiles/health
Liveness probe → {"ok":true,"root_exists":true}.

Path parameters

ParamTypeRangeMeaning
zint2 – 16zoom level
xint0 … 2z−1tile column (west→east)
yint0 … 2z−1tile row (north→south, XYZ origin top-left)

Use in a map (Leaflet)

// 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.

Caching

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

Performance & throughput

Tile rendering is CPU-bound (numpy + Pillow under Python's GIL), so the way to avoid slow tiles / timeouts is:

For Claude / LLM agents

Compact machine-readable spec for driving this service:

API summary (JSON)
{
  "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).

Decode: CM93 is C-MAP's proprietary, obfuscated vector format — no GDAL/QGIS driver exists. This service decodes it directly (ported from OpenCPN's GPL cm93.cpp) and renders the main chart classes. Not yet parsed: attribute values (depth labels, light characteristics) and depth-graduated water shading.