# Tilemap Server A static XYZ raster-tile server for maps supplied as KML/KMZ `GroundOverlay` files. All sources are composited into one global layer; the URL contains no map identifier. ## Architecture `tile-builder` recursively reads `source/`, extracts KMZ files only into temporary directories, reads each `GroundOverlay`, finds the local image referenced by `Icon/href`, and reprojects it to Web Mercator (EPSG:3857). For a `LatLonBox` with `rotation`, its four corners are rotated counter-clockwise around the center before the GDAL warp. The final raster retains alpha to preserve transparency and clip rotated edges. Each overlay produces temporary tiles with `gdal2tiles --xyz`. The builder composites them with alpha into a single pyramid. A smaller area takes precedence and is rendered on top; where areas are equal, the relative path in alphabetical order wins, followed by the `GroundOverlay` index. Area is calculated from the rotated polygon in EPSG:3857. The generated files are published directly under the local `tiles/` directory: ```text tiles/{z}/{x}/{y}.png tiles/metadata.json ``` Nginx accepts only the public route below and translates it internally. Do not publish or use the storage order directly. ```text https://tiles.example.com/tiles/{x}/{y}/{z} https://tiles.example.com/tiles/18342/12417/15 ``` Even without `.png` in the URL, the response uses `Content-Type: image/png` and includes `Access-Control-Allow-Origin: *`, so it can be used directly by MapLibre from another origin. Without a configured provider, uncovered coordinates return `tiles/empty.png`; with a provider, they are forwarded to the callback and cached locally by Nginx. ## Callback provider and MapLibre Copy `.env.example` to `.env` and, for a base layer, define an HTTPS XYZ template: ```dotenv CALLBACK_PROVIDER=https://tiles.example-provider.com/{z}/{x}/{y}.png ``` The template is validated before source processing begins. It must use `https`, cannot include a query, credentials, or a private IP address, and must contain exactly one `{z}`, `{x}`, and `{y}` in its path. Leave the variable empty or unset to use the white `empty.png`. To show the provider both outside map coverage and through the transparent edges of partially covered tiles, place the local base layer below the overlay layer: ```js sources: { providerBase: { type: "raster", tiles: ["http://localhost:9000/base/{x}/{y}/{z}"], tileSize: 256, scheme: "xyz" }, overlays: { type: "raster", tiles: ["http://localhost:9000/tiles/{x}/{y}/{z}"], tileSize: 256, scheme: "xyz", minzoom: 0, maxzoom: 17 } }, layers: [ { id: "provider-base", type: "raster", source: "providerBase" }, { id: "overlays", type: "raster", source: "overlays" } ] ``` `/base/{x}/{y}/{z}` and the `/tiles/` fallback request the callback on demand and share a persistent local cache. Display attribution and comply with the selected provider's terms. ## Inputs Place `.kmz` and `.kml` files in `source/`, including subdirectories. A KMZ must contain one KML file (the builder prefers `doc.kml`) and the local image referenced by the KML. Standalone KML files may reference images within the same `source/` tree. Input files are never modified or removed. Per-file errors are recorded in `tiles/metadata.json` and do not prevent other sources from being processed. HTTP(S) links, absolute paths, and `gx:LatLonQuad` are not supported in this first version; use `LatLonBox` and local images. ## Zoom and configuration The automatic maximum zoom for each overlay is calculated from the highest reprojected resolution in metres per pixel. The builder selects the highest tile zoom whose resolution is no more detailed than the source, avoiding pixel upscaling. The automatic minimum zoom is `0`: broad levels are inexpensive and make isolated maps discoverable from a world view. Each source stops at its native maximum, avoiding server-side pixel upscaling. To allow extra visual zoom, configure MapLibre with a high `maxZoom` and retain the source's native `maxzoom`; MapLibre will enlarge the last available tile. If no source covers the coordinate, the server uses the configured callback or white `empty.png`. `config/maps.json` supports global and source-relative overrides: ```json { "global": { "minzoom": null, "maxzoom": null }, "sources": { "example.kmz": { "minzoom": 8, "maxzoom": 16 }, "subfolder/example.kml": { "maxzoom": 14 } } } ``` Use an integer between 0 and 22 or `null`. Precedence is source, global, then automatic calculation. An explicit `maxzoom` above the automatic maximum is allowed and is marked in `metadata.json`, since it deliberately requests upscaling. ## Local build Local dependencies: Python 3, Pillow, and GDAL with `gdal_translate`, `gdalwarp`, `gdalinfo`, and `gdal2tiles.py`. ```bash ./scripts/build-tiles.sh ./scripts/build-tiles.sh --source-filter example.kmz ``` The `cache/` directory uses the source SHA-256. Unchanged inputs reuse their reprojected GeoTIFFs. To ensure correct compositing and priority, version 1 rebuilds the entire final pyramid in staging for every build; only reprojection of unchanged sources is reused. At the end of a build, the local `tiles/` directory is replaced with the complete result after `metadata.json` is generated. ## Docker Compose and Coolify For local development: ```bash docker compose up --build ``` Nginx is available at `http://localhost:${HTTP_PORT:-9000}`. Compose runs the builder first and mounts `tiles/` read-only in Nginx. In Coolify, keep inputs outside Git and create a persistent host directory or volume. Set the source-directory environment variable to that path; it is mounted read-only at `/app/source`. The named data volume preserves the cache between deployments, and the project's `tiles/` directory is the output volume published by both the builder and Nginx. Configure your domain and TLS in the Coolify proxy, targeting port 80 of the `tiles` service. To limit GDAL parallelism, set `GDAL2TILES_PROCESSES` (default: `1`). Tile responses have one day of public caching plus seven days of `stale-while-revalidate`, as well as an ETag. ## Validation ```bash python3 tests/test_build_tiles.py ./scripts/smoke-test.sh ``` The smoke test processes the bundled sample KMZ, checks `tiles/metadata.json` and a PNG, starts Nginx, and requests a tile using the public `{x}/{y}/{z}` order to confirm `200`, `image/png`, and CORS. The callback is optional and is not exercised by the local test. ## Known limitations - There is no watcher: after adding or changing an input, run the builder or deploy again. - Final compositing is still a global rebuild; the cache structure allows future invalidation of only affected tiles. - This version does not download external images, support `gx:LatLonQuad`, handle overlays crossing the antimeridian, or include DEM, hillshade, vectors, MBTiles, a frontend, authentication, or a database.