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.
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:
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.
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.
Copy .env.example to .env and, for a base layer, define an HTTPS XYZ template:
CALLBACK_PROVIDER=https://tiles.example-provider.com/{z}/{x}/{y}.pngThe 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.
During generation, every tile with Arma coverage is flattened over the callback tile. Therefore MapLibre needs only one source:
sources: {
arma: {
type: "raster",
tiles: ["http://localhost:9000/tiles/{x}/{y}/{z}"],
tileSize: 256,
scheme: "xyz",
minzoom: 0,
maxzoom: 17
}
},
layers: [{ id: "arma", type: "raster", source: "arma" }]Only /tiles/{x}/{y}/{z} is public. Tiles without Arma coverage are fetched on demand from the callback and cached by Nginx. The builder also caches callback tiles while flattening covered areas. Display attribution and comply with the selected provider's terms.
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.
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. This repository sets global maxzoom to 17, deliberately allowing enlargement above the native resolution so a map does not disappear into the callback while zooming. Set it back to null to keep the automatic per-overlay maximum. 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:
{
"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 dependencies: Python 3, Pillow, and GDAL with gdal_translate, gdalwarp, gdalinfo, and gdal2tiles.py.
./scripts/build-tiles.sh
./scripts/build-tiles.sh --source-filter example.kmzThe 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.
For local development:
docker compose up --buildNginx 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.
python3 tests/test_build_tiles.py
./scripts/smoke-test.shThe 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.
- 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.