diff --git a/src/utils/cesiumImagery.ts b/src/utils/cesiumImagery.ts index 37de6d1..b355abf 100644 --- a/src/utils/cesiumImagery.ts +++ b/src/utils/cesiumImagery.ts @@ -141,6 +141,63 @@ function cleanupWarpedFile(warpedFilePath: string): void { }); } +/** + * Returns the path to the extents metadata file inside a tile cache directory. + */ +function getExtentsMetadataPath(tileCacheDir: string): string { + return path.join(tileCacheDir, ".extents.json"); +} + +/** + * Checks if valid cached tiles and extents exist for a given tile cache directory. + * Returns the cached extents if found, or null if no valid cache exists. + */ +function getCachedExtents(tileCacheDir: string): CesiumRectDeg | null { + const metadataPath = getExtentsMetadataPath(tileCacheDir); + try { + if (!fs.existsSync(metadataPath)) { + return null; + } + + const data = JSON.parse(fs.readFileSync(metadataPath, "utf-8")); + if ( + data.west === undefined || + data.south === undefined || + data.east === undefined || + data.north === undefined + ) { + return null; + } + + // Verify at least one zoom-level directory exists alongside the metadata + const entries = fs.readdirSync(tileCacheDir); + const hasZoomDirs = entries.some(entry => /^\d+$/.test(entry)); + if (!hasZoomDirs) { + return null; + } + + logger.info("Found cached tiles with extents:", data); + return data as CesiumRectDeg; + } catch (err) { + logger.debug("No valid tile cache found:", err); + return null; + } +} + +/** + * Persists extents metadata alongside cached tiles for future reuse. + */ +function saveCachedExtents(tileCacheDir: string, extents: CesiumRectDeg): void { + const metadataPath = getExtentsMetadataPath(tileCacheDir); + try { + fs.mkdirSync(tileCacheDir, { recursive: true }); + fs.writeFileSync(metadataPath, JSON.stringify(extents, null, 2), "utf-8"); + logger.debug("Saved tile cache metadata:", metadataPath); + } catch (err) { + logger.error("Failed to save tile cache metadata:", err); + } +} + /** * Converts a local image to Cesium tiles using Docker-based ctb-tile. * Images are first warped to EPSG:4326 via gdalwarp to handle projected @@ -155,9 +212,29 @@ export async function convertImageToCesium( fileName = fileName.replace(/^(\.\.(\/|\\|$))+/, ""); const imageFolder = path.resolve(LOCAL_IMAGE_DATA_FOLDER); const tileFolder = path.resolve(CESIUM_IMAGERY_TILES_FOLDER); + const baseName = path.parse(fileName).name; + const tileCacheDir = path.join(tileFolder, baseName); const warpedFileName = "_warped.tif"; const warpedFilePath = path.join(imageFolder, warpedFileName); + // Check for cached tiles before running the Docker pipeline + const cachedExtents = getCachedExtents(tileCacheDir); + if (cachedExtents) { + logger.info("Using cached tiles for:", fileName); + const tileBaseUrl = typeof window !== "undefined" + ? window.location.origin + : FALLBACK_DEV_URL; + return addImageLayerWithExtents( + cesium.viewer, + `${tileBaseUrl}/src/data/tiles/imagery/${baseName}/{z}/{x}/{reverseY}.png`, + imageId, + cachedExtents, + setShowCredsExpiredAlert + ); + } + + logger.info("No cached tiles found for:", fileName, "- generating tiles..."); + try { return await new Promise((resolve, reject) => { exec("docker pull tumgis/ctb-quantized-mesh:alpine", async (err) => { @@ -203,9 +280,9 @@ export async function convertImageToCesium( const extents = JSON.parse(extentsOutput.trim()); logger.info("Successfully calculated extents:", extents); - // Step 3: Generate tiles from the warped image + // Step 3: Generate tiles into a per-image subdirectory exec( - `docker exec ${jobName} sh -c "chmod -R 755 /data/tiles/ && ctb-tile -f PNG -R -C -N -s ${ZOOM_MAX} -e ${ZOOM_MIN} -t 256 -o /data/tiles /data/images/${warpedFileName}"`, + `docker exec ${jobName} sh -c "mkdir -p /data/tiles/${baseName} && chmod -R 755 /data/tiles/ && ctb-tile -f PNG -R -C -N -s ${ZOOM_MAX} -e ${ZOOM_MIN} -t 256 -o /data/tiles/${baseName} /data/images/${warpedFileName}"`, async (err) => { if (err) { cleanupWarpedFile(warpedFilePath); @@ -215,12 +292,15 @@ export async function convertImageToCesium( } try { + // Persist extents so subsequent loads skip the Docker pipeline + saveCachedExtents(tileCacheDir, extents); + const tileBaseUrl = typeof window !== "undefined" ? window.location.origin : FALLBACK_DEV_URL; const layer = await addImageLayerWithExtents( cesium.viewer, - `${tileBaseUrl}/src/data/tiles/imagery/{z}/{x}/{reverseY}.png`, + `${tileBaseUrl}/src/data/tiles/imagery/${baseName}/{z}/{x}/{reverseY}.png`, imageId, extents, setShowCredsExpiredAlert