You are modifying the Azgaar’s Fantasy Map Generator codebase to add a new “GeoTIFF Tile Pyramid (16×16)” export option that replaces the current SVG tile export path when selected. The goal is to produce a set of 256 GeoTIFF tiles (16×16 at full resolution) with correct georeferencing so they can be used as a basemap in QGIS and Leaflet. Include an optional GDAL post-step to convert tiles to COG and build internal overviews and a VRT mosaic.
-
Add a new menu item alongside existing exports:
- Export → GeoTIFF Tile Pyramid (16×16).
-
A small modal with options:
- CRS: default
EPSG:4326; - Bounds: default to full map extent.
- Output resolution (pixels wide × high of the full map before tiling).
- Compression:
NONE | LZW | DEFLATE | ZSTD(defaultDEFLATE). - Block size:
256 | 512(default512). - Nodata: numeric (default
0or leave unset). - Make COG + Overviews (GDAL): checkbox (off by default).
- Download as zip (browser path) OR Generate job file (.fmgpack) (for a Node/GDAL post-processor).
- CRS: default
Implement two cooperating pieces:
-
Client-side exporter (browser, no native deps):
- File:
modules/export/geotiffPyramid.js(or similar next to svg export). - Renders the current FMG map canvas into a full-resolution raster (the same visual composition used for SVG tiles), then cuts it into 16×16 equal tiles.
- For each tile, writes a GeoTIFF with correct GeoTransform and CRS using geotiff.js (or a small GeoTIFF encoder lib).
- Packs all tiles into a ZIP (streamed) so the user downloads
geotiff_pyramid_16x16.zip. - Also writes a mosaic VRT (
mosaic.vrt) and a TileJSON (tilejson.json) into the ZIP. - If “Make COG + Overviews (GDAL)” is checked, instead of a ZIP of final GeoTIFFs, export a job bundle (
.fmgpackJSON + raw tile TIFFs optional) that a Node CLI converts to COG + overviews.
- File:
-
Optional Node+GDAL post-processor (for perfect COGs & overviews):
-
Files:
tools/export-fmg-pyramid-cli.js(CLI)tools/lib/gdalPyramid.js(helpers)
-
Input: the
.fmgpackjob file (contains: bounds, CRS, grid=16, tile file names, desired compression/blocksize/overviews, nodata). -
Output: a folder with:
tiles/r{row}_c{col}.tif(COG with internal overviews),mosaic.vrtbuilt withgdalbuildvrt,tilejson.jsonwith metadata,- (optional) a final ZIP.
-
Keep the existing SVG export intact; this adds a parallel GeoTIFF option that uses the same visual styling stack FMG uses to render the final “map appearance.”
-
CRS: hard-code WGS84 / EPSG:4326 for all exports. No other CRS, no reprojection.
-
Inputs (from FMG/export UI):
canvasWidthPx,canvasHeightPx— the rendered map’s pixel size.- Scale option A:
pixelSizeKm(kilometres per pixel). or - Scale option B:
mapWidthKm,mapHeightKm(total map size in km) → derivepixelSizeKm. centerLonDeg,centerLatDeg(defaults0, 0; allow user override).nodata(optional),compression,blockSize, etc. as before.
-
Convert km ↔ degrees (WGS84 approximations):
kmPerDegLat(φ) ≈ 110.574 kmPerDegLon(φ) ≈ 111.320 · cos(φ)where φ =
centerLatDegin radians. -
Derive degrees per pixel:
degPerPxLat = pixelSizeKm / kmPerDegLat(centerLatDeg) degPerPxLon = pixelSizeKm / kmPerDegLon(centerLatDeg)If using total map size:
pixelSizeKmX = mapWidthKm / canvasWidthPx pixelSizeKmY = mapHeightKm / canvasHeightPx # usually pixelSizeKmX == pixelSizeKmY; if not, compute deg per px separately: degPerPxLon = pixelSizeKmX / kmPerDegLon(centerLatDeg) degPerPxLat = pixelSizeKmY / kmPerDegLat(centerLatDeg) -
Full map extent in degrees (north-up, EPSG:4326):
widthDeg = canvasWidthPx · degPerPxLon heightDeg = canvasHeightPx · degPerPxLat minx = centerLonDeg - widthDeg / 2 maxx = centerLonDeg + widthDeg / 2 miny = centerLatDeg - heightDeg / 2 maxy = centerLatDeg + heightDeg / 2 -
16×16 tiling (no gaps/overlaps):
tilesX = tilesY = 16 tileWidthPx = canvasWidthPx / tilesX tileHeightPx = canvasHeightPx / tilesY tileWidthDeg = tileWidthPx · degPerPxLon tileHeightDeg = tileHeightPx · degPerPxLatFor tile at (row r, col c) with r,c ∈ [0..15]:
tileMinX = minx + c · tileWidthDeg tileMaxY = maxy - r · tileHeightDeg GeoTransform = [ tileMinX, # GT[0] top-left lon degPerPxLon, # GT[1] pixel width (deg/pixel, +east) 0, # GT[2] tileMaxY, # GT[3] top-left lat 0, # GT[4] -degPerPxLat # GT[5] pixel height (deg/pixel, -south) ]This guarantees 16×16 tiles exactly partition
[minx,maxx] × [miny,maxy]with no rounding gaps (ensure all math in double precision; only round when writing tags if the library forces it). -
Tile naming:
tiles/r{r}_c{c}.tif(r,c zero-based). -
Tags & data layout:
-
Always embed
EPSG:4326in the GeoTIFF GeoKeys. -
Set NoData if provided.
-
Bit depth: 8-bit RGBA for styled basemap tiles (DEM export, if added later, must be 32-bit float).
-
If producing COGs in a post-step, use internal tiling (
BLOCKSIZE=512) and build overviews (2 4 8 16 32), resampling:averagefor continuous rasters,nearestfor categorical (biomes, labels).
-
-
UI nits (update export dialog):
- Remove CRS/WKT fields (since EPSG:4326 is fixed).
- Add either Pixel size (km/px) or Map size (km) fields.
- Add optional Center latitude/longitude (defaults 0/0). This affects the longitude km/deg conversion; documenting this avoids surprises.
-
Validation:
-
After writing,
gdalinfoon a few tiles must show:Coordinate System is: EPSG:4326- Corner coords that match
tileMinX, tileMaxYand increments ofdegPerPxLon/Lat.
-
Build a
mosaic.vrtover all 256 tiles; in QGIS the mosaic must align perfectly with a WGS84 grid.
-
const toRad = (d) => (d * Math.PI) / 180;
const kmPerDegLat = 110.574;
const kmPerDegLon = (latDeg) => 111.32 * Math.cos(toRad(latDeg));
function georefFromCanvas({
canvasWidthPx,
canvasHeightPx,
pixelSizeKm = null,
mapWidthKm = null,
mapHeightKm = null,
centerLonDeg = 0,
centerLatDeg = 0,
}) {
const kx = mapWidthKm ?? pixelSizeKm * canvasWidthPx;
const ky = mapHeightKm ?? pixelSizeKm * canvasHeightPx;
const degPerPxLon = kx / canvasWidthPx / kmPerDegLon(centerLatDeg);
const degPerPxLat = ky / canvasHeightPx / kmPerDegLat;
const widthDeg = canvasWidthPx * degPerPxLon;
const heightDeg = canvasHeightPx * degPerPxLat;
const minx = centerLonDeg - widthDeg / 2;
const maxy = centerLatDeg + heightDeg / 2;
const tiles = [];
const tilesX = 16,
tilesY = 16;
const tileWidthPx = canvasWidthPx / tilesX;
const tileHeightPx = canvasHeightPx / tilesY;
for (let r = 0; r < tilesY; r++)
for (let c = 0; c < tilesX; c++) {
const tileMinX = minx + c * tileWidthPx * degPerPxLon;
const tileMaxY = maxy - r * tileHeightPx * degPerPxLat;
const GT = [tileMinX, degPerPxLon, 0, tileMaxY, 0, -degPerPxLat];
tiles.push({ r, c, geotransform: GT });
}
return { tiles, degPerPxLon, degPerPxLat };
}-
Rendering: render the exact visible FMG map (layers, styles, labels as desired) into an OffscreenCanvas or a hidden canvas at the target full resolution (e.g., 16384×16384).
-
Cutting: draw sub-rects into a tile canvas of size
fullWidth/16 × fullHeight/16and encode that tile to a GeoTIFF. -
GeoTIFF writing:
-
Use geotiff.js (or equivalent) to create a baseline GeoTIFF with tags:
ModelPixelScaleTag,ModelTiepointTag(or fullGeoTransform),GeoKeyDirectoryTag(CRS),TIFFTAG_IMAGEDESCRIPTIONwith FMG metadata.
-
Creation options to mimic tiling: internal strip/tiling is not strictly required client-side; do what geotiff.js supports.
-
Compression: if library supports
LZW/Deflate, apply per user; else fallback to none and let GDAL post-step recompress to COG.
-
-
Packaging: stream tiles +
mosaic.vrt+tilejson.jsoninto a ZIP for download OR create.fmgpackif GDAL step is requested.
-
CLI usage:
node tools/export-fmg-pyramid-cli.js \ --job /path/to/job.fmgpack \ --out /path/to/outdir \ --make-cog \ --overviews 2,4,8,16,32 \ --compression ZSTD \ --blocksize 512 \ --nodata 0
-
Process:
-
For each input tile (if raw PNGs/PNMs were produced by browser), run
gdal_translateto GeoTIFF with:-co TILED=YES -co BLOCKXSIZE=512 -co BLOCKYSIZE=512 -co COMPRESS=ZSTD -co BIGTIFF=YES-a_srsset from job CRS;-a_ullrset to tile geographic bounds.
-
If
--make-cog, convert to COG:gdal_translate -of COG -co COMPRESS=ZSTD -co BLOCKSIZE=512 in.tif out.tif
-
Build overviews:
gdaladdo -r average out.tif 2 4 8 16 32(ornearestfor categorical).
-
Build VRT mosaic over all final TIFFs:
gdalbuildvrt mosaic.vrt tiles/*.tif
-
Write TileJSON (
tilejson.json) with:{ "tilejson":"2.2.0", "name":"FMG GeoTIFF Pyramid 16x16", "crs":"EPSG:4326", "bounds":[minx,miny,maxx,maxy], "grid_dim":16, "overview_levels":[2,4,8,16,32], "compression":"ZSTD", "tiles":["tiles/r{row}_c{col}.tif"] }
-
-
New module:
modules/export/geotiffPyramid.js-
Export function:
export async function exportGeoTiffPyramid16x16(options) { // options: { crs, wktOrProj, bounds, fullWidth, fullHeight, compression, blockSize, nodata, makeCOG } // 1) render full canvas // 2) slice to 16×16 tiles // 3) write GeoTIFFs (geotiff.js) // 4) generate mosaic.vrt + tilejson.json // 5) zip or .fmgpack }
-
-
Hook: in the existing export UI code (where SVG tiles are offered), add:
- “GeoTIFF Tile Pyramid (16×16)” → calls the new function.
-
Shared helpers:
modules/export/geoTags.jsfor CRS/WKT handling and GeoKeys.modules/export/vrtWriter.jsto emit a simple VRT mosaic referencing tile geotransforms.modules/export/tilejsonWriter.js.
-
Exporting a 16×16 GeoTIFF pyramid on a standard FMG map produces:
- 256 GeoTIFF files with correct georeferencing (CRS + bounds).
- A mosaic.vrt that opens in QGIS and visually matches the FMG map.
- A TileJSON manifest with accurate metadata.
-
Optional Node+GDAL step:
- Produces COG tiles with internal overviews at specified levels.
gdalinfoshowsCoordinate System is ..., correctCorner Coordinates, andOverviews:lines.
-
No regressions in existing SVG/PNG exports.
-
The operation is stable for large canvases (e.g., 16k×16k full map) by streaming/tiling, not holding the entire ZIP in memory at once.
- If browser-side GeoTIFF compression is limited, prefer the .fmgpack path for COG + overviews; that gives production-grade tiles for QGIS/Leaflet.
- Keep label layers optional; many users will want a basemap without labels and a separate vector/label layer in QGIS.
Implement this feature now.