/** * Reconstruct a full map image from a Leaflet tile pyramid (zoom level z). * * Usage: * npx tsx pipeline/assemble-map.ts [zoom-level] * * The tiles-dir must contain subdirectories named by x-index, each containing * y-index.jpg files (standard Leaflet tile layout: {z}/{x}/{y}.jpg). * * Example: * npx tsx pipeline/assemble-map.ts pipeline/source/aventurien-tiles-z6 pipeline/source/aventurien.jpg */ import sharp from 'sharp'; import { readdirSync, existsSync } from 'fs'; import { join, resolve } from 'path'; const TILE_SIZE = 256; async function assembleTiles(tilesDir: string, outputPath: string) { if (!existsSync(tilesDir)) { console.error(`Tiles directory not found: ${tilesDir}`); process.exit(1); } // Discover grid dimensions from directory structure const xDirs = readdirSync(tilesDir) .map(Number) .filter(n => !isNaN(n)) .sort((a, b) => a - b); if (xDirs.length === 0) { console.error('No tile columns found in', tilesDir); process.exit(1); } const maxX = Math.max(...xDirs); // Count rows from first column const firstColDir = join(tilesDir, String(xDirs[0])); const yFiles = readdirSync(firstColDir) .map(f => parseInt(f)) .filter(n => !isNaN(n)) .sort((a, b) => a - b); const maxY = Math.max(...yFiles); const tilesX = maxX + 1; const tilesY = maxY + 1; const totalWidth = tilesX * TILE_SIZE; const totalHeight = tilesY * TILE_SIZE; console.log(`Grid: ${tilesX}×${tilesY} tiles → canvas ${totalWidth}×${totalHeight}px`); const composites: sharp.OverlayOptions[] = []; let loaded = 0; for (const x of xDirs) { const colDir = join(tilesDir, String(x)); const yFiles = readdirSync(colDir) .map(f => parseInt(f)) .filter(n => !isNaN(n)) .sort((a, b) => a - b); for (const y of yFiles) { const tilePath = join(colDir, `${y}.jpg`); if (!existsSync(tilePath)) continue; composites.push({ input: tilePath, left: x * TILE_SIZE, top: y * TILE_SIZE, }); loaded++; } } console.log(`Compositing ${loaded} tiles...`); // Process in batches to avoid hitting sharp's composite limit const BATCH = 200; let canvas = sharp({ create: { width: totalWidth, height: totalHeight, channels: 3, background: { r: 42, g: 85, b: 116 } }, }).jpeg({ quality: 92 }); // sharp supports all composites in one call for reasonable counts await sharp({ create: { width: totalWidth, height: totalHeight, channels: 3, background: { r: 42, g: 85, b: 116 } }, }) .composite(composites) .jpeg({ quality: 92 }) .toFile(outputPath); console.log(`Assembled → ${outputPath}`); // Print map config hint const actualW = tilesX * TILE_SIZE; const actualH = tilesY * TILE_SIZE; console.log(`\nMap config hint:`); console.log(` imageSize: [${actualW}, ${actualH}]`); console.log(` PIXELS_PER_MEILE: 8`); console.log(` 10 Meilen/Hex → hexSize: 40 (~${Math.round(actualW/80)}×${Math.round(actualH/69)} hexes)`); console.log(` 5 Meilen/Hex → hexSize: 20 (~${Math.round(actualW/40)}×${Math.round(actualH/35)} hexes)`); } const [tilesDir, outputPath] = process.argv.slice(2); if (!tilesDir || !outputPath) { console.error('Usage: npx tsx pipeline/assemble-map.ts '); process.exit(1); } assembleTiles(resolve(tilesDir), resolve(outputPath)).catch(err => { console.error('Failed:', err); process.exit(1); });