From 9e2a5ce803d735e6ee0c104ee181024911defc29 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 15:56:14 -0500 Subject: [PATCH 01/12] copy in code --- packages/deck.gl-cog/src/cog-tileset.ts | 0 packages/deck.gl-raster/package.json | 4 +- .../deck.gl-raster/src/raster-tile-layer.ts | 0 .../src/raster-tileset/index.ts | 0 .../raster-tileset/raster-tile-traversal.ts | 531 +++++++++++++++++ .../src/raster-tileset/raster-tileset-2d.ts | 344 +++++++++++ .../src/raster-tileset/types.ts | 340 +++++++++++ pnpm-lock.yaml | 551 ++++++++++++++++++ 8 files changed, 1769 insertions(+), 1 deletion(-) create mode 100644 packages/deck.gl-cog/src/cog-tileset.ts create mode 100644 packages/deck.gl-raster/src/raster-tile-layer.ts create mode 100644 packages/deck.gl-raster/src/raster-tileset/index.ts create mode 100644 packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts create mode 100644 packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts create mode 100644 packages/deck.gl-raster/src/raster-tileset/types.ts diff --git a/packages/deck.gl-cog/src/cog-tileset.ts b/packages/deck.gl-cog/src/cog-tileset.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/deck.gl-raster/package.json b/packages/deck.gl-raster/package.json index cbc1f9a8..ff032da2 100644 --- a/packages/deck.gl-raster/package.json +++ b/packages/deck.gl-raster/package.json @@ -53,11 +53,13 @@ }, "peerDependencies": { "@deck.gl/core": "^9.2.5", + "@deck.gl/geo-layers": "^9.2.5", "@deck.gl/layers": "^9.2.5", "@deck.gl/mesh-layers": "^9.2.5" }, "dependencies": { - "@developmentseed/raster-reproject": "workspace:^" + "@developmentseed/raster-reproject": "workspace:^", + "@math.gl/culling": "^4.1.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/deck.gl-raster/src/raster-tile-layer.ts b/packages/deck.gl-raster/src/raster-tile-layer.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/deck.gl-raster/src/raster-tileset/index.ts b/packages/deck.gl-raster/src/raster-tileset/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts new file mode 100644 index 00000000..92e583d7 --- /dev/null +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts @@ -0,0 +1,531 @@ +/** + * This file implements tile traversal for generic 2D tilesets defined by COG + * tile layouts. + * + * The main algorithm works as follows: + * 1. Start at the root tile(s) (z=0, covers the entire image, but not + * necessarily the whole world) + * 2. Test if each tile is visible using viewport frustum culling + * 3. For visible tiles, compute distance-based LOD (Level of Detail) + * 4. If LOD is insufficient, recursively subdivide into 4 child tiles + * 5. Select tiles at appropriate zoom levels based on distance from camera + * + * The result is a set of tiles at varying zoom levels that efficiently + * cover the visible area with appropriate detail. + */ + +import { + _GlobeViewport, + assert, + Viewport, + WebMercatorViewport, +} from "@deck.gl/core"; +import { + CullingVolume, + Plane, + makeOrientedBoundingBoxFromPoints, +} from "@math.gl/culling"; + +import type { + COGMetadata, + COGOverview, + COGTileIndex, + ZRange, +} from "./types.js"; + +/** + * The size of the entire world in deck.gl's common coordinate space. + * + * The world always spans [0, 512] in both X and Y in Web Mercator common space. + * + * The origin (0,0) is at the top-left corner, and (512,512) is at the + * bottom-right. + */ +const WORLD_SIZE = 512; + +// Reference points used to sample tile boundaries for bounding volume +// calculation. +// +// In upstream deck.gl code, such reference points are only used in non-Web +// Mercator projections because the OSM tiling scheme is designed for Web +// Mercator and the OSM tile extents are already in Web Mercator projection. So +// using Axis-Aligned bounding boxes based on tile extents is sufficient for +// frustum culling in Web Mercator viewports. +// +// In upstream code these reference points are used for Globe View where the OSM +// tile indices _projected into longitude-latitude bounds in Globe View space_ +// are no longer axis-aligned, and oriented bounding boxes must be used instead. +// +// In the context of generic tiling grids which are often not in Web Mercator +// projection, we must use the reference points approach because the grid tiles +// will never be exact axis aligned boxes in Web Mercator space. + +// For most tiles: sample 4 corners and center (5 points total) +const REF_POINTS_5 = [ + [0.5, 0.5], // center + [0, 0], // top-left + [0, 1], // bottom-left + [1, 0], // top-right + [1, 1], // bottom-right +]; + +// For higher detail: add 4 edge midpoints (9 points total) +const REF_POINTS_9 = REF_POINTS_5.concat([ + [0, 0.5], // left edge + [0.5, 0], // top edge + [1, 0.5], // right edge + [0.5, 1], // bottom edge +]); + +/** + * COG Tile Node - similar to OSMNode but for COG's tile structure. + * + * Represents a single tile in the COG internal tiling pyramid. + * + * COG tile nodes use the following coordinate system: + * + * - x: tile column (0 to COGOverview.tilesX, left to right) + * - y: tile row (0 to COGOverview.tilesY, top to bottom) + * - z: overview level. This uses TileMatrixSet ordering where: 0 = coarsest, higher = finer + */ +export class COGTileNode { + /** Index across a row */ + x: number; + /** Index down a column */ + y: number; + /** TileMatrixSet-style zoom index (higher = finer detail) */ + z: number; + + private cogMetadata: COGMetadata; + + /** + * Flag indicating whether any descendant of this tile is visible. + * + * Used to prevent loading parent tiles when children are visible (avoids + * overdraw). + */ + private childVisible?: boolean; + + /** + * Flag indicating this tile should be rendered + * + * Set to `true` when this is the appropriate LOD for its distance from camera. + */ + private selected?: boolean; + + /** A cache of the children of this node. */ + private _children?: COGTileNode[]; + + constructor(x: number, y: number, z: number, cogMetadata: COGMetadata) { + this.x = x; + this.y = y; + this.z = z; + this.cogMetadata = cogMetadata; + } + + /** Get overview info for this tile's z level */ + get overview(): COGOverview { + return this.cogMetadata.overviews[this.z]; + } + + /** Get the children of this node. */ + get children(): COGTileNode[] { + if (!this._children) { + const maxZ = this.cogMetadata.overviews.length - 1; + if (this.z >= maxZ) { + // Already at finest resolution, no children + return []; + } + + // In TileMatrixSet ordering: refine to z + 1 (finer detail) + const childZ = this.z + 1; + const parentOverview = this.overview; + const childOverview = this.cogMetadata.overviews[childZ]; + + // Calculate scale factor between levels + const scaleFactor = + parentOverview.scaleFactor / childOverview.scaleFactor; + + // Generate child tiles + this._children = []; + for (let dy = 0; dy < scaleFactor; dy++) { + for (let dx = 0; dx < scaleFactor; dx++) { + const childX = this.x * scaleFactor + dx; + const childY = this.y * scaleFactor + dy; + + // Only create child if it's within bounds + // Some tiles on the edges might not need to be created at higher + // resolutions (higher map zoom level) + if (childX < childOverview.tilesX && childY < childOverview.tilesY) { + this._children.push( + new COGTileNode(childX, childY, childZ, this.cogMetadata), + ); + } + } + } + } + return this._children; + } + + /** + * Update tile visibility using frustum culling + * This follows the pattern from OSMNode + */ + update(params: { + viewport: Viewport; + project: ((xyz: number[]) => number[]) | null; + cullingVolume: CullingVolume; + elevationBounds: ZRange; + /** Minimum (coarsest) COG overview level */ + minZ: number; + /** Maximum (finest) COG overview level */ + maxZ?: number; + }): boolean { + const { + viewport, + cullingVolume, + elevationBounds, + minZ, + maxZ = this.cogMetadata.overviews.length - 1, + project, + } = params; + + // Get bounding volume for this tile + const boundingVolume = this.getBoundingVolume(elevationBounds, project); + + // Note: this is a part of the upstream code because they have _generic_ + // tiling systems, where the client doesn't know whether a given xyz tile + // actually exists. So the idea of `bounds` is to avoid even trying to fetch + // tiles that the user doesn't care about (think oceans) + // + // But in our case, we have known bounds from the COG metadata. So the tiles + // are explicitly constructed to match only tiles that exist. + + // Check if tile is within user-specified bounds + // if (bounds && !this.insideBounds(bounds)) { + // return false; + // } + + console.log("=== FRUSTUM CULLING DEBUG ==="); + console.log(`Tile: ${this.x}, ${this.y}, ${this.z}`); + console.log("Bounding volume center:", boundingVolume.center); + console.log("Bounding volume halfSize:", boundingVolume.halfSize); + console.log("Viewport cameraPosition:", viewport.cameraPosition); + console.log( + "Viewport pitch:", + viewport instanceof WebMercatorViewport ? viewport.pitch : "N/A", + ); + + for (let i = 0; i < cullingVolume.planes.length; i++) { + const plane = cullingVolume.planes[i]; + const result = boundingVolume.intersectPlane(plane); + const planeNames = ["left", "right", "bottom", "top", "near", "far"]; + + // Calculate signed distance from OBB center to plane + const centerDist = + plane.normal.x * boundingVolume.center.x + + plane.normal.y * boundingVolume.center.y + + plane.normal.z * boundingVolume.center.z + + plane.distance; + + console.log( + `Plane ${i} (${planeNames[i]}): normal=[${plane.normal.x.toFixed(3)}, ${plane.normal.y.toFixed(3)}, ${plane.normal.z.toFixed(3)}], ` + + `distance=${plane.distance.toFixed(3)}, centerDist=${centerDist.toFixed(3)}, result=${result} (${result === 1 ? "INSIDE" : result === 0 ? "INTERSECT" : "OUTSIDE"})`, + ); + } + console.log("=== END FRUSTUM DEBUG ==="); + + // Frustum culling + // Test if tile's bounding volume intersects the camera frustum + // Returns: <0 if outside, 0 if intersecting, >0 if fully inside + const isInside = cullingVolume.computeVisibility(boundingVolume); + console.log( + `Tile ${this.x},${this.y},${this.z} frustum check: ${isInside} (${isInside < 0 ? "CULLED" : "VISIBLE"})`, + ); + if (isInside < 0) { + return false; + } + + // LOD (Level of Detail) selection + // Only select this tile if no child is visible (prevents overlapping tiles) + if (!this.childVisible) { + let { z } = this; + + if (z < maxZ && z >= minZ) { + // Compute distance-based LOD adjustment + // Tiles farther from camera can use lower zoom levels (larger tiles) + // Distance is normalized by viewport height to be resolution-independent + const distance = + (boundingVolume.distanceTo(viewport.cameraPosition) * + viewport.scale) / + viewport.height; + // Increase effective zoom level based on log2(distance) + // e.g., if distance=4, accept tiles 2 levels lower than maxZ + z += Math.floor(Math.log2(distance)); + } + + if (z >= maxZ) { + // This tile's LOD is sufficient for its distance - select it for rendering + this.selected = true; + return true; + } + } + + // LOD is not enough, recursively test child tiles + this.selected = false; + this.childVisible = true; + + for (const child of this.children) { + child.update(params); + } + + // // NOTE: this deviates from upstream; we could move to the upstream code if + // // we pass in maxZ correctly I think + // if (children.length === 0) { + // // No children available (at finest resolution), select this tile + // this.selected = true; + // return true; + // } + + // for (const child of children) { + // child.update(params); + // } + return true; + } + + /** + * Collect all tiles marked as selected in the tree. + * Recursively traverses the entire tree and gathers tiles where selected=true. + * + * @param result - Accumulator array for selected tiles + * @returns Array of selected OSMNode tiles + */ + getSelected(result: COGTileNode[] = []): COGTileNode[] { + if (this.selected) { + result.push(this); + } + if (this._children) { + for (const node of this._children) { + node.getSelected(result); + } + } + return result; + } + + /** + * Calculate the 3D bounding volume for this tile in deck.gl's common + * coordinate space for frustum culling. + * + */ + getBoundingVolume( + zRange: ZRange, + project: ((xyz: number[]) => number[]) | null, + ) { + const overview = this.overview; + const { tileWidth, tileHeight } = this.cogMetadata; + + // Use geotransform to calculate tile bounds + // geotransform: [a, b, c, d, e, f] where: + // x_geo = a * col + b * row + c + // y_geo = d * col + e * row + f + const [a, b, c, d, e, f] = overview.geotransform; + + // Calculate pixel coordinates for this tile's extent + const pixelMinCol = this.x * tileWidth; + const pixelMinRow = this.y * tileHeight; + const pixelMaxCol = (this.x + 1) * tileWidth; + const pixelMaxRow = (this.y + 1) * tileHeight; + + // Sample reference points across the tile surface + const refPoints = REF_POINTS_9; + + console.log("refPoints", refPoints); + + /** Reference points positions in image CRS */ + const refPointPositionsImage: number[][] = []; + + for (const [pX, pY] of refPoints) { + // pX, pY are in [0, 1] range + // Interpolate pixel coordinates within the tile + const col = pixelMinCol + pX * (pixelMaxCol - pixelMinCol); + const row = pixelMinRow + pY * (pixelMaxRow - pixelMinRow); + + // Convert pixel coordinates to geographic coordinates using geotransform + const geoX = a * col + b * row + c; + const geoY = d * col + e * row + f; + + refPointPositionsImage.push([geoX, geoY]); + } + + console.log("refPointPositionsImage (image CRS):", refPointPositionsImage); + console.log("Geotransform [a,b,c,d,e,f]:", [a, b, c, d, e, f]); + + if (project) { + assert( + false, + "TODO: implement bounding volume implementation in Globe view", + ); + // Reproject positions to wgs84 instead, then pass them into `project` + // return makeOrientedBoundingBoxFromPoints(refPointPositions); + } + + /** Reference points positions in EPSG 3857 */ + const refPointPositionsProjected: number[][] = []; + + for (const [pX, pY] of refPointPositionsImage) { + // Reproject to Web Mercator (EPSG 3857) + const projected = this.cogMetadata.projectTo3857.forward([pX, pY]); + refPointPositionsProjected.push(projected); + + // Also log WGS84 for comparison + const wgs84 = this.cogMetadata.projectToWgs84.forward([pX, pY]); + console.log( + `Image [${pX.toFixed(2)}, ${pY.toFixed(2)}] -> WGS84 [${wgs84[0].toFixed(6)}, ${wgs84[1].toFixed(6)}] -> WebMerc [${projected[0].toFixed(2)}, ${projected[1].toFixed(2)}]`, + ); + } + + console.log( + "refPointPositionsProjected (EPSG:3857):", + refPointPositionsProjected, + ); + + // Convert from Web Mercator meters to deck.gl's common space (world units) + // Web Mercator range: [-20037508.34, 20037508.34] meters + // deck.gl world space: [0, 512] + const WEB_MERCATOR_MAX = 20037508.342789244; // Half Earth circumference + + /** Reference points positions in deck.gl world space */ + const refPointPositionsWorld: number[][] = []; + + for (const [mercX, mercY] of refPointPositionsProjected) { + // X: offset from [-20M, 20M] to [0, 40M], then normalize to [0, 512] + const worldX = + ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + // Y: same transformation WITHOUT flip + // Testing hypothesis: Y-flip might be incorrect since geotransform already handles orientation + const worldY = + ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + console.log( + `WebMerc [${mercX.toFixed(2)}, ${mercY.toFixed(2)}] -> World [${worldX.toFixed(4)}, ${worldY.toFixed(4)}]`, + ); + + // Add z-range minimum + refPointPositionsWorld.push([worldX, worldY, zRange[0]]); + } + + // Add top z-range if elevation varies + if (zRange[0] !== zRange[1]) { + for (const [mercX, mercY] of refPointPositionsProjected) { + const worldX = + ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + const worldY = + WORLD_SIZE - + ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; + + refPointPositionsWorld.push([worldX, worldY, zRange[1]]); + } + } + + console.log("refPointPositionsWorld", refPointPositionsWorld); + console.log("zRange used:", zRange); + + const obb = makeOrientedBoundingBoxFromPoints(refPointPositionsWorld); + console.log("Created OBB center:", obb.center); + console.log("Created OBB halfAxes:", obb.halfAxes); + + return obb; + } + + /** + * Convert COG coordinates to lng/lat + * This is a placeholder - needs proper projection library (proj4js) + */ + private cogCoordsToLngLat([x, y]: [number, number]): number[] { + const [lng, lat] = this.cogMetadata.projectToWgs84.forward([x, y]); + return [lng, lat, 0]; + } +} + +/** + * Get tile indices visible in viewport + * Uses frustum culling similar to OSM implementation + * + * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer + */ +export function getTileIndices( + cogMetadata: COGMetadata, + opts: { + viewport: Viewport; + maxZ: number; + zRange: ZRange | null; + }, +): COGTileIndex[] { + const { viewport, maxZ, zRange } = opts; + + // console.log("=== getTileIndices called ==="); + // console.log("Viewport:", viewport); + // console.log("maxZ:", maxZ); + // console.log("COG metadata overviews count:", cogMetadata.overviews.length); + // console.log("COG bbox:", cogMetadata.bbox); + + const project: ((xyz: number[]) => number[]) | null = + viewport instanceof _GlobeViewport && viewport.resolution + ? viewport.projectPosition + : null; + + // Get the culling volume of the current camera + const planes: Plane[] = Object.values(viewport.getFrustumPlanes()).map( + ({ normal, distance }) => new Plane(normal.clone().negate(), distance), + ); + const cullingVolume = new CullingVolume(planes); + + // Project zRange from meters to common space + const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2]; + const elevationMin = (zRange && zRange[0] * unitsPerMeter) || 0; + const elevationMax = (zRange && zRange[1] * unitsPerMeter) || 0; + + // Optimization: For low-pitch views, only consider tiles at maxZ level + // At low pitch (top-down view), all tiles are roughly the same distance, + // so we don't need the LOD pyramid - just use the finest level + const minZ = + viewport instanceof WebMercatorViewport && viewport.pitch <= 60 ? maxZ : 0; + + // Start from coarsest overview + const coarsestOverview = cogMetadata.overviews[0]; + + // Create root tiles at coarsest level + // In contrary to OSM tiling, we might have more than one tile at the + // coarsest level (z=0) + const roots: COGTileNode[] = []; + for (let y = 0; y < coarsestOverview.tilesY; y++) { + for (let x = 0; x < coarsestOverview.tilesX; x++) { + roots.push(new COGTileNode(x, y, 0, cogMetadata)); + } + } + + // Traverse and update visibility + const traversalParams = { + viewport, + project, + cullingVolume, + elevationBounds: [elevationMin, elevationMax] as ZRange, + minZ, + maxZ, + }; + console.log("Traversal params:", traversalParams); + + for (const root of roots) { + root.update(traversalParams); + } + console.log("roots", roots); + + // Collect selected tiles + const selectedNodes: COGTileNode[] = []; + for (const root of roots) { + root.getSelected(selectedNodes); + } + + return selectedNodes; +} diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts new file mode 100644 index 00000000..e3e8ccfe --- /dev/null +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts @@ -0,0 +1,344 @@ +/** + * COGTileset2D - Improved Implementation with Frustum Culling + * + * This version properly implements frustum culling and bounding volume calculations + * following the pattern from deck.gl's OSM tile indexing. + */ + +import { Viewport, WebMercatorViewport } from "@deck.gl/core"; +import { _Tileset2D as Tileset2D } from "@deck.gl/geo-layers"; +import type { Tileset2DProps } from "@deck.gl/geo-layers/dist/tileset-2d"; +import type { ZRange } from "@deck.gl/geo-layers/dist/tileset-2d/types"; +import { Matrix4 } from "@math.gl/core"; +import { GeoTIFF, GeoTIFFImage } from "geotiff"; +import proj4 from "proj4"; + +import { getTileIndices } from "./raster-tile-traversal"; +import type { COGMetadata, COGTileIndex, COGOverview, Bounds } from "./types"; + +const OGC_84 = { + $schema: "https://proj.org/schemas/v0.7/projjson.schema.json", + type: "GeographicCRS", + name: "WGS 84 (CRS84)", + datum_ensemble: { + name: "World Geodetic System 1984 ensemble", + members: [ + { + name: "World Geodetic System 1984 (Transit)", + id: { authority: "EPSG", code: 1166 }, + }, + { + name: "World Geodetic System 1984 (G730)", + id: { authority: "EPSG", code: 1152 }, + }, + { + name: "World Geodetic System 1984 (G873)", + id: { authority: "EPSG", code: 1153 }, + }, + { + name: "World Geodetic System 1984 (G1150)", + id: { authority: "EPSG", code: 1154 }, + }, + { + name: "World Geodetic System 1984 (G1674)", + id: { authority: "EPSG", code: 1155 }, + }, + { + name: "World Geodetic System 1984 (G1762)", + id: { authority: "EPSG", code: 1156 }, + }, + { + name: "World Geodetic System 1984 (G2139)", + id: { authority: "EPSG", code: 1309 }, + }, + ], + ellipsoid: { + name: "WGS 84", + semi_major_axis: 6378137, + inverse_flattening: 298.257223563, + }, + accuracy: "2.0", + id: { authority: "EPSG", code: 6326 }, + }, + coordinate_system: { + subtype: "ellipsoidal", + axis: [ + { + name: "Geodetic longitude", + abbreviation: "Lon", + direction: "east", + unit: "degree", + }, + { + name: "Geodetic latitude", + abbreviation: "Lat", + direction: "north", + unit: "degree", + }, + ], + }, + scope: "Not known.", + area: "World.", + bbox: { + south_latitude: -90, + west_longitude: -180, + north_latitude: 90, + east_longitude: 180, + }, + id: { authority: "OGC", code: "CRS84" }, +}; + +/** + * Extract COG metadata + */ +export async function extractCOGMetadata(tiff: GeoTIFF): Promise { + const image = await tiff.getImage(); + + const width = image.getWidth(); + const height = image.getHeight(); + const tileWidth = image.getTileWidth(); + const tileHeight = image.getTileHeight(); + + const tilesX = Math.ceil(width / tileWidth); + const tilesY = Math.ceil(height / tileHeight); + + const bbox = image.getBoundingBox(); + const geoKeys = image.getGeoKeys(); + const projectionCode: number | null = + geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || null; + const projection = projectionCode ? `EPSG:${projectionCode}` : null; + + // Extract geotransform from full-resolution image + // Only the top-level IFD has geo keys, so we'll derive overviews from this + const baseGeotransform = extractGeotransform(image); + + // Overviews **in COG order**, from finest to coarsest (we'll reverse the + // array later) + const overviews: COGOverview[] = []; + const imageCount = await tiff.getImageCount(); + + // Full resolution image (GeoTIFF index 0) + overviews.push({ + geoTiffIndex: 0, + width, + height, + tilesX, + tilesY, + scaleFactor: 1, + geotransform: baseGeotransform, + // TODO: combine these two properties into one + level: imageCount - 1, // Coarsest level number + z: imageCount - 1, + }); + + for (let i = 1; i < imageCount; i++) { + const overview = await tiff.getImage(i); + const overviewWidth = overview.getWidth(); + const overviewHeight = overview.getHeight(); + const overviewTileWidth = overview.getTileWidth(); + const overviewTileHeight = overview.getTileHeight(); + + const scaleFactor = Math.round(width / overviewWidth); + + // Derive geotransform for this overview by scaling pixel size + // [a, b, c, d, e, f] where a and e are pixel dimensions + const overviewGeotransform: [ + number, + number, + number, + number, + number, + number, + ] = [ + baseGeotransform[0] * scaleFactor, // a: scaled pixel width + baseGeotransform[1] * scaleFactor, // b: scaled row rotation + baseGeotransform[2], // c: same x origin + baseGeotransform[3] * scaleFactor, // d: scaled column rotation + baseGeotransform[4] * scaleFactor, // e: scaled pixel height (typically negative) + baseGeotransform[5], // f: same y origin + ]; + + overviews.push({ + geoTiffIndex: i, + width: overviewWidth, + height: overviewHeight, + tilesX: Math.ceil(overviewWidth / overviewTileWidth), + tilesY: Math.ceil(overviewHeight / overviewTileHeight), + scaleFactor, + geotransform: overviewGeotransform, + // TODO: combine these two properties into one + level: imageCount - 1 - i, + z: imageCount - 1 - i, + }); + } + + // Reverse to TileMatrixSet order: coarsest (0) → finest (n) + overviews.reverse(); + + const sourceProjection = await getProjjson(projectionCode); + const projectToWgs84 = proj4(sourceProjection, OGC_84); + const projectTo3857 = proj4(sourceProjection, "EPSG:3857"); + + return { + width, + height, + tileWidth, + tileHeight, + tilesX, + tilesY, + bbox: [bbox[0], bbox[1], bbox[2], bbox[3]], + projection, + projectToWgs84, + projectTo3857, + overviews, + image: tiff, + }; +} + +async function getProjjson(projectionCode: number | null) { + const url = `https://epsg.io/${projectionCode}.json`; + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch projection data from ${url}`); + } + const data = await response.json(); + return data; +} + +const viewport = new WebMercatorViewport({ + height: 500, + width: 845, + latitude: 40.88775942857086, + longitude: -73.20197979318772, + zoom: 11.294596276534985, +}); + +/** + * COGTileset2D with proper frustum culling + */ +export class COGTileset2D extends Tileset2D { + private cogMetadata: COGMetadata; + + constructor(cogMetadata: COGMetadata, opts: Tileset2DProps) { + super(opts); + this.cogMetadata = cogMetadata; + } + + /** + * Get tile indices visible in viewport + * Uses frustum culling similar to OSM implementation + * + * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer + */ + getTileIndices(opts: { + viewport: Viewport; + maxZoom?: number; + minZoom?: number; + zRange: ZRange | null; + modelMatrix?: Matrix4; + modelMatrixInverse?: Matrix4; + }): COGTileIndex[] { + console.log("Called getTileIndices", opts); + const tileIndices = getTileIndices(this.cogMetadata, opts); + console.log("Visible tile indices:", tileIndices); + + // return [ + // { x: 0, y: 0, z: 0 }, + // { x: 0, y: 0, z: 1 }, + // { x: 1, y: 1, z: 2 }, + // { x: 1, y: 2, z: 3 }, + // { x: 2, y: 1, z: 3 }, + // { x: 2, y: 2, z: 3 }, + // { x: 3, y: 1, z: 3 }, + // ]; // Temporary override for testing + return tileIndices; + } + + getTileId(index: COGTileIndex): string { + return `${index.x}-${index.y}-${index.z}`; + } + + getParentIndex(index: COGTileIndex): COGTileIndex { + if (index.z === 0) { + // Already at coarsest level + return index; + } + + const currentOverview = this.cogMetadata.overviews[index.z]; + const parentOverview = this.cogMetadata.overviews[index.z - 1]; + + const scaleFactor = + currentOverview.scaleFactor / parentOverview.scaleFactor; + + return { + x: Math.floor(index.x / scaleFactor), + y: Math.floor(index.y / scaleFactor), + z: index.z - 1, + }; + } + + getTileZoom(index: COGTileIndex): number { + return index.z; + } + + getTileMetadata(index: COGTileIndex): Record { + const { x, y, z } = index; + const { overviews, tileWidth, tileHeight } = this.cogMetadata; + const overview = overviews[z]; + + // Use geotransform to calculate tile bounds + // geotransform: [a, b, c, d, e, f] where: + // x_geo = a * col + b * row + c + // y_geo = d * col + e * row + f + const [a, b, c, d, e, f] = overview.geotransform; + + // Calculate pixel coordinates for this tile's extent + const pixelMinCol = x * tileWidth; + const pixelMinRow = y * tileHeight; + const pixelMaxCol = (x + 1) * tileWidth; + const pixelMaxRow = (y + 1) * tileHeight; + + // Calculate the four corners of the tile in geographic coordinates + const topLeft = [ + a * pixelMinCol + b * pixelMinRow + c, + d * pixelMinCol + e * pixelMinRow + f, + ]; + const topRight = [ + a * pixelMaxCol + b * pixelMinRow + c, + d * pixelMaxCol + e * pixelMinRow + f, + ]; + const bottomLeft = [ + a * pixelMinCol + b * pixelMaxRow + c, + d * pixelMinCol + e * pixelMaxRow + f, + ]; + const bottomRight = [ + a * pixelMaxCol + b * pixelMaxRow + c, + d * pixelMaxCol + e * pixelMaxRow + f, + ]; + + // Return the projected bounds as four corners + // This preserves rotation/skew information + const projectedBounds = { + topLeft, + topRight, + bottomLeft, + bottomRight, + }; + + // Also compute axis-aligned bounding box for compatibility + const bounds: Bounds = [ + Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), + Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), + ]; + + return { + bounds, + projectedBounds, + tileWidth, + tileHeight, + overview, + }; + } +} diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts new file mode 100644 index 00000000..75a3156e --- /dev/null +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -0,0 +1,340 @@ +export type ZRange = [minZ: number, maxZ: number]; + +export type Bounds = [minX: number, minY: number, maxX: number, maxY: number]; + +export type GeoBoundingBox = { + west: number; + north: number; + east: number; + south: number; +}; +export type NonGeoBoundingBox = { + left: number; + top: number; + right: number; + bottom: number; +}; + +export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; + +export type TileIndex = { x: number; y: number; z: number }; + +export type TileLoadProps = { + index: TileIndex; + id: string; + bbox: TileBoundingBox; + url?: string | null; + signal?: AbortSignal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + userData?: Record; + zoom?: number; +}; + +//////////////// +// Claude-generated metadata +//////////////// + +/** + * Represents a single resolution level in a Cloud Optimized GeoTIFF. + * + * COGs contain multiple resolution levels (overviews) for efficient + * visualization at different zoom levels. + * + * IMPORTANT: Overviews are ordered according to TileMatrixSet specification: + * - Index 0: Coarsest resolution (most zoomed out) + * - Index N: Finest resolution (most zoomed in) + * + * This matches the natural ordering where z increases with detail. + */ +export type COGOverview = { + /** + * Overview index in the TileMatrixSet ordering. + * - Index 0: Coarsest resolution (most zoomed out) + * - Higher indices: Progressively finer resolution + * + * This is the index in the COGMetadata.overviews array and represents + * the natural ordering from coarse to fine. + * + * Note: This is different from GeoTIFF's internal level numbering where + * level 0 is the full resolution image. + * + * @example + * // For a COG with 4 resolutions: + * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) + * index: 1 // Medium: 2500x2000 pixels (4x downsampled) + * index: 2 // Fine: 5000x4000 pixels (2x downsampled) + * index: 3 // Finest: 10000x8000 pixels (full resolution) + */ + level: number; + + /** + * Zoom index (OSM convention). + * Defined as: maxLevel - currentLevel + * + * This makes the code compatible with OSM tile indexing where: + * - Higher z = finer detail (opposite of COG level) + * - Lower z = coarser detail + * + * In TileMatrixSet ordering: z === level (both increase with detail) + */ + z: number; + + /** + * Width of the entire image at this overview level, in pixels. + */ + width: number; + /** + * Height of the entire image at this overview level, in pixels. + */ + height: number; + + /** + * Number of tiles in the X (horizontal) direction at this overview level. + * + * Calculated as: Math.ceil(width / tileWidth) + * + * @example + * // If tileWidth = 512: + * tilesX: 3 // z=0: ceil(1250 / 512) + * tilesX: 5 // z=1: ceil(2500 / 512) + * tilesX: 10 // z=2: ceil(5000 / 512) + * tilesX: 20 // z=3: ceil(10000 / 512) + */ + tilesX: number; + + /** + * Number of tiles in the Y (vertical) direction at this overview level. + * + * Calculated as: Math.ceil(height / tileHeight) + * + * @example + * // If tileHeight = 512: + * tilesY: 2 // z=0: ceil(1000 / 512) + * tilesY: 4 // z=1: ceil(2000 / 512) + * tilesY: 8 // z=2: ceil(4000 / 512) + * tilesY: 16 // z=3: ceil(8000 / 512) + */ + tilesY: number; + + /** + * Downsampling scale factor relative to full resolution (finest level). + * + * Indicates how much this overview is downsampled compared to the finest resolution. + * - Scale factor of 1: Full resolution (finest level) + * - Scale factor of 2: Half resolution + * - Scale factor of 4: Quarter resolution + * - Scale factor of 8: Eighth resolution (coarsest in this example) + * + * Common pattern: Each overview is 2x downsampled from the next finer level, + * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) + * + * @example + * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) + * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) + * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) + * scaleFactor: 1 // z=3: 10000x8000 (full resolution) + */ + scaleFactor: number; + + /** + * Index in the original GeoTIFF file. + * + * GeoTIFF stores: image 0 = full resolution, image 1+ = overviews (progressively coarser) + * This index is needed to read the correct image from the GeoTIFF file. + * + * Note: This may differ from `level` since we reorder overviews to TileMatrixSet order. + * + * @example + * // TileMatrixSet order (our array): + * level: 0, geoTiffIndex: 3 // Coarsest (GeoTIFF overview 3) + * level: 1, geoTiffIndex: 2 // Medium (GeoTIFF overview 2) + * level: 2, geoTiffIndex: 1 // Fine (GeoTIFF overview 1) + * level: 3, geoTiffIndex: 0 // Finest (GeoTIFF main image) + */ + geoTiffIndex: number; + + /** + * Affine geotransform for this overview level. + * + * Uses Python `affine` package ordering (NOT GDAL ordering): + * [a, b, c, d, e, f] where: + * - x_geo = a * col + b * row + c + * - y_geo = d * col + e * row + f + * + * Parameters: + * - a: pixel width (x resolution) + * - b: row rotation (typically 0) + * - c: x-coordinate of upper-left corner of the upper-left pixel + * - d: column rotation (typically 0) + * - e: pixel height (y resolution, typically negative) + * - f: y-coordinate of upper-left corner of the upper-left pixel + * + * @example + * // For a UTM image with 30m pixels: + * [30, 0, 440720, 0, -30, 3751320] + * // x_geo = 30 * col + 440720 + * // y_geo = -30 * row + 3751320 + */ + geotransform: [number, number, number, number, number, number]; +}; + +/** + * COG Metadata extracted from GeoTIFF + */ +export type COGMetadata = { + width: number; + height: number; + /** Number of pixels wide for each tile */ + tileWidth: number; + tileHeight: number; + tilesX: number; + tilesY: number; + bbox: Bounds; // COG's CRS + projection: string | null; + overviews: COGOverview[]; + image: GeoTIFF; // GeoTIFF reference + projectToWgs84: Converter; + projectTo3857: Converter; +}; + +/** + * COG Tile Index + * + * In TileMatrixSet ordering: level === z (both 0 = coarsest, higher = finer) + */ +export type COGTileIndex = { + x: number; + y: number; + z: number; // TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) +}; + +//////////////// +// TileMatrixSet +//////////////// + +// type CRS = string | { [k: string]: unknown }; +export type TMSCrs = unknown; + +/** + * A 2D Point in the CRS indicated elsewhere + * + * @minItems 2 + * @maxItems 2 + */ +export type TMSPoint = [number, number]; + +/** + * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere + */ +export interface TMSBoundingBox { + lowerLeft: TMSPoint; + upperRight: TMSPoint; + crs?: TMSCrs; + /** + * @minItems 2 + * @maxItems 2 + */ + orderedAxes?: [string, string]; + [k: string]: unknown; +} + +/** + * A definition of a tile matrix set following the Tile Matrix Set standard. For tileset metadata, such a description (in `tileMatrixSet` property) is only required for offline use, as an alternative to a link with a `http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme` relation type. + */ +export type TileMatrixSetDefinition = { + /** + * Title of this tile matrix set, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set + */ + keywords?: string[]; + /** + * Tile matrix set identifier. Implementation of 'identifier' + */ + id?: string; + /** + * Reference to an official source for this tileMatrixSet + */ + uri?: string; + /** + * @minItems 1 + */ + orderedAxes?: [string, ...string[]]; + crs: TMSCrs; + /** + * Reference to a well-known scale set + */ + wellKnownScaleSet?: string; + boundingBox?: { + [k: string]: unknown; + } & TMSBoundingBox; + /** + * Describes scale levels and its tile matrices + */ + tileMatrices: TMSTileMatrix[]; + [k: string]: unknown; +}; + +/** + * A tile matrix, usually corresponding to a particular zoom level of a TileMatrixSet. + */ +export interface TMSTileMatrix { + /** + * Title of this tile matrix, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + /** + * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset + */ + keywords?: string[]; + /** + * Identifier selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile. Implementation of 'identifier' + */ + id: string; + /** + * Scale denominator of this tile matrix + */ + scaleDenominator: number; + /** + * Cell size of this tile matrix + */ + cellSize: number; + /** + * The corner of the tile matrix (_topLeft_ or _bottomLeft_) used as the origin for numbering tile rows and columns. This corner is also a corner of the (0, 0) tile. + */ + cornerOfOrigin?: "topLeft" | "bottomLeft"; + pointOfOrigin: { + [k: string]: unknown; + } & TMSPoint; + /** + * Width of each tile of this tile matrix in pixels + */ + tileWidth: number; + /** + * Height of each tile of this tile matrix in pixels + */ + tileHeight: number; + /** + * Width of the matrix (number of tiles in width) + */ + matrixHeight: number; + /** + * Height of the matrix (number of tiles in height) + */ + matrixWidth: number; + /** + * Describes the rows that has variable matrix width + */ + variableMatrixWidths?: object[]; + [k: string]: unknown; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce7150a1..a7096c98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@deck.gl/core': specifier: ^9.2.5 version: 9.2.5 + '@deck.gl/geo-layers': + specifier: ^9.2.5 + version: 9.2.5(@deck.gl/core@9.2.5)(@deck.gl/extensions@9.2.5(@deck.gl/core@9.2.5)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))))(@deck.gl/layers@9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))))(@deck.gl/mesh-layers@9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/gltf@9.2.4(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@loaders.gl/core@4.3.4)(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))) '@deck.gl/layers': specifier: ^9.2.5 version: 9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))) @@ -130,6 +133,9 @@ importers: '@developmentseed/raster-reproject': specifier: workspace:^ version: link:../raster-reproject + '@math.gl/culling': + specifier: ^4.1.0 + version: 4.1.0 devDependencies: '@types/node': specifier: ^25.0.1 @@ -360,6 +366,24 @@ packages: '@deck.gl/core@9.2.5': resolution: {integrity: sha512-/PGNX4Wd7rEahYi6ivC4WExJ3U6Hqgl42R83guNzTL6gM2+02PUQRoQG9QdFagj5d6kWYVN0LVJME2a5WQmzOg==} + '@deck.gl/extensions@9.2.5': + resolution: {integrity: sha512-GJRPmG+GD1tdblpplQlb4jlNywRb8aQYPEowPLKxglXSGRzgpOrqJYI1PcJhCowdL7/S8bCY1ay8nkXE3gRsgw==} + peerDependencies: + '@deck.gl/core': ~9.2.0 + '@luma.gl/core': ~9.2.4 + '@luma.gl/engine': ~9.2.4 + + '@deck.gl/geo-layers@9.2.5': + resolution: {integrity: sha512-QVjLwHEAtNqRdjBuYJwztCAwSTmgWujPT0geGWaFhr7ZvyigAqi3l2ETys5YqjjJ87bKnUBwd6iOyw1xkXbsCw==} + peerDependencies: + '@deck.gl/core': ~9.2.0 + '@deck.gl/extensions': ~9.2.0 + '@deck.gl/layers': ~9.2.0 + '@deck.gl/mesh-layers': ~9.2.0 + '@loaders.gl/core': ^4.2.0 + '@luma.gl/core': ~9.2.4 + '@luma.gl/engine': ~9.2.4 + '@deck.gl/layers@9.2.5': resolution: {integrity: sha512-a48zWxeHknSX67ZeIzWeLXuOVJkEnQjnLIC27Uv3zHjKlvaoraWPOgScUNyteB0UIIzhzE+D8lF+ViHeIdkNSA==} peerDependencies: @@ -611,14 +635,34 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@loaders.gl/3d-tiles@4.3.4': + resolution: {integrity: sha512-JQ3y3p/KlZP7lfobwON5t7H9WinXEYTvuo3SRQM8TBKhM+koEYZhvI2GwzoXx54MbBbY+s3fm1dq5UAAmaTsZw==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + + '@loaders.gl/compression@4.3.4': + resolution: {integrity: sha512-+o+5JqL9Sx8UCwdc2MTtjQiUHYQGJALHbYY/3CT+b9g/Emzwzez2Ggk9U9waRfdHiBCzEgRBivpWZEOAtkimXQ==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/core@4.3.4': resolution: {integrity: sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==} + '@loaders.gl/crypto@4.3.4': + resolution: {integrity: sha512-3VS5FgB44nLOlAB9Q82VOQnT1IltwfRa1miE0mpHCe1prYu1M/dMnEyynusbrsp+eDs3EKbxpguIS9HUsFu5dQ==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/draco@4.3.4': resolution: {integrity: sha512-4Lx0rKmYENGspvcgV5XDpFD9o+NamXoazSSl9Oa3pjVVjo+HJuzCgrxTQYD/3JvRrolW/QRehZeWD/L/cEC6mw==} peerDependencies: '@loaders.gl/core': ^4.3.0 + '@loaders.gl/gis@4.3.4': + resolution: {integrity: sha512-8xub38lSWW7+ZXWuUcggk7agRHJUy6RdipLNKZ90eE0ZzLNGDstGD1qiBwkvqH0AkG+uz4B7Kkiptyl7w2Oa6g==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/gltf@4.3.4': resolution: {integrity: sha512-EiUTiLGMfukLd9W98wMpKmw+hVRhQ0dJ37wdlXK98XPeGGB+zTQxCcQY+/BaMhsSpYt/OOJleHhTfwNr8RgzRg==} peerDependencies: @@ -634,21 +678,56 @@ packages: peerDependencies: '@loaders.gl/core': ^4.3.0 + '@loaders.gl/math@4.3.4': + resolution: {integrity: sha512-UJrlHys1fp9EUO4UMnqTCqvKvUjJVCbYZ2qAKD7tdGzHJYT8w/nsP7f/ZOYFc//JlfC3nq+5ogvmdpq2pyu3TA==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + + '@loaders.gl/mvt@4.3.4': + resolution: {integrity: sha512-9DrJX8RQf14htNtxsPIYvTso5dUce9WaJCWCIY/79KYE80Be6dhcEYMknxBS4w3+PAuImaAe66S5xo9B7Erm5A==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/schema@4.3.4': resolution: {integrity: sha512-1YTYoatgzr/6JTxqBLwDiD3AVGwQZheYiQwAimWdRBVB0JAzych7s1yBuE0CVEzj4JDPKOzVAz8KnU1TiBvJGw==} peerDependencies: '@loaders.gl/core': ^4.3.0 + '@loaders.gl/terrain@4.3.4': + resolution: {integrity: sha512-JszbRJGnxL5Fh82uA2U8HgjlsIpzYoCNNjy3cFsgCaxi4/dvjz3BkLlBilR7JlbX8Ka+zlb4GAbDDChiXLMJ/g==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/textures@4.3.4': resolution: {integrity: sha512-arWIDjlE7JaDS6v9by7juLfxPGGnjT9JjleaXx3wq/PTp+psLOpGUywHXm38BNECos3MFEQK3/GFShWI+/dWPw==} peerDependencies: '@loaders.gl/core': ^4.3.0 + '@loaders.gl/tiles@4.3.4': + resolution: {integrity: sha512-oC0zJfyvGox6Ag9ABF8fxOkx9yEFVyzTa9ryHXl2BqLiQoR1v3p+0tIJcEbh5cnzHfoTZzUis1TEAZluPRsHBQ==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + + '@loaders.gl/wms@4.3.4': + resolution: {integrity: sha512-yXF0wuYzJUdzAJQrhLIua6DnjOiBJusaY1j8gpvuH1VYs3mzvWlIRuZKeUd9mduQZKK88H2IzHZbj2RGOauq4w==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@loaders.gl/worker-utils@4.3.4': resolution: {integrity: sha512-EbsszrASgT85GH3B7jkx7YXfQyIYo/rlobwMx6V3ewETapPUwdSAInv+89flnk5n2eu2Lpdeh+2zS6PvqbL2RA==} peerDependencies: '@loaders.gl/core': ^4.3.0 + '@loaders.gl/xml@4.3.4': + resolution: {integrity: sha512-p+y/KskajsvyM3a01BwUgjons/j/dUhniqd5y1p6keLOuwoHlY/TfTKd+XluqfyP14vFrdAHCZTnFCWLblN10w==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + + '@loaders.gl/zip@4.3.4': + resolution: {integrity: sha512-bHY4XdKYJm3vl9087GMoxnUqSURwTxPPh6DlAGOmz6X9Mp3JyWuA2gk3tQ1UIuInfjXKph3WAUfGe6XRIs1sfw==} + peerDependencies: + '@loaders.gl/core': ^4.3.0 + '@luma.gl/constants@9.2.4': resolution: {integrity: sha512-Cy0OAg3uJbbHhPJGeik6ZhII0EMokTXmo6MtP7dyUrS+pSG5N176G4WYD9zS4DaJm2cLUW4UJzsu5B4Cd8rBuw==} @@ -687,6 +766,12 @@ packages: resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} engines: {node: '>= 0.6'} + '@mapbox/martini@0.2.0': + resolution: {integrity: sha512-7hFhtkb0KTLEls+TRw/rWayq5EeHtTaErgm/NskVoXmtgAQu/9D299aeyj6mzAR/6XUnYRp2lU+4IcrYRFjVsQ==} + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + '@mapbox/point-geometry@1.1.0': resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} @@ -696,6 +781,9 @@ packages: '@mapbox/unitbezier@0.0.1': resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + '@mapbox/vector-tile@1.3.1': + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + '@mapbox/vector-tile@2.0.4': resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} @@ -720,6 +808,12 @@ packages: '@math.gl/core@4.1.0': resolution: {integrity: sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==} + '@math.gl/culling@4.1.0': + resolution: {integrity: sha512-jFmjFEACnP9kVl8qhZxFNhCyd47qPfSVmSvvjR0/dIL6R9oD5zhR1ub2gN16eKDO/UM7JF9OHKU3EBIfeR7gtg==} + + '@math.gl/geospatial@4.1.0': + resolution: {integrity: sha512-BzsUhpVvnmleyYF6qdqJIip6FtIzJmnWuPTGhlBuPzh7VBHLonCFSPtQpbkRuoyAlbSyaGXcVt6p6lm9eK2vtg==} + '@math.gl/polygon@4.1.0': resolution: {integrity: sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==} @@ -864,6 +958,24 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@turf/boolean-clockwise@5.1.5': + resolution: {integrity: sha512-FqbmEEOJ4rU4/2t7FKx0HUWmjFEVqR+NJrFP7ymGSjja2SQ7Q91nnBihGuT+yuHHl6ElMjQ3ttsB/eTmyCycxA==} + + '@turf/clone@5.1.5': + resolution: {integrity: sha512-//pITsQ8xUdcQ9pVb4JqXiSqG4dos5Q9N4sYFoWghX21tfOV2dhc5TGqYOhnHrQS7RiKQL1vQ48kIK34gQ5oRg==} + + '@turf/helpers@5.1.5': + resolution: {integrity: sha512-/lF+JR+qNDHZ8bF9d+Cp58nxtZWJ3sqFe6n3u3Vpj+/0cqkjk4nXKYBSY0azm+GIYB5mWKxUXvuP/m0ZnKj1bw==} + + '@turf/invariant@5.2.0': + resolution: {integrity: sha512-28RCBGvCYsajVkw2EydpzLdcYyhSA77LovuOvgCJplJWaNVyJYH6BOR3HR9w50MEkPqb/Vc/jdo6I6ermlRtQA==} + + '@turf/meta@5.2.0': + resolution: {integrity: sha512-ZjQ3Ii62X9FjnK4hhdsbT+64AYRpaI8XMBMcyftEOGSmPMUVnkbvuv3C9geuElAXfQU7Zk1oWGOcrGOD9zr78Q==} + + '@turf/rewind@5.1.5': + resolution: {integrity: sha512-Gdem7JXNu+G4hMllQHXRFRihJl3+pNl7qY+l4qhQFxq+hiU1cQoVFnyoleIqWKIrdK/i2YubaSwc3SCM7N5mMw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -876,9 +988,15 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/brotli@1.3.4': + resolution: {integrity: sha512-cKYjgaS2DMdCKF7R0F5cgx1nfBYObN2ihIuPGQ4/dlIY6RpV7OWNwe9L8V4tTVKL2eZqOkNM9FM/rgTvLf4oXw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -900,6 +1018,9 @@ packages: '@types/offscreencanvas@2019.7.3': resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/pako@1.0.7': + resolution: {integrity: sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1028,6 +1149,9 @@ packages: '@zarrita/storage@0.1.3': resolution: {integrity: sha512-ZyCMYN3LuCNtKxro9876r/KyHyXV+ie2Bhk1qYsJR4Jp+sAjoVRRNNSJPsJxk64ZgFFezayO5S2hCu88/1Odwg==} + a5-js@0.5.0: + resolution: {integrity: sha512-VAw19sWdYadhdovb0ViOIi1SdKx6H6LwcGMRFKwMfgL5gcmL/1fKJHfgsNgNaJ7xC/eEyjs6VK+VVd4N0a+peg==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1073,6 +1197,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + baseline-browser-mapping@2.9.7: resolution: {integrity: sha512-k9xFKplee6KIio3IDbwj+uaCLpqzOwakOgmqzPezM0sFJlFKcg30vk2wOiAJtkTSfx0SSQDSe8q+mWA/fSH5Zg==} hasBin: true @@ -1086,11 +1213,18 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buf-compare@1.0.1: + resolution: {integrity: sha512-Bvx4xH00qweepGc43xFvMs5BKASXTbHaHm6+kDYIK9p/4iFwjATQkmPKHQSgJZzKbAymhztRbXUf1Nqhzl73/Q==} + engines: {node: '>=0.10.0'} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1122,6 +1256,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1150,10 +1287,20 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + core-assert@0.2.1: + resolution: {integrity: sha512-IG97qShIP+nrJCXMCgkNZgH7jZQ4n8RpPyPeXX++T6avR/KhLhgLiHKoEn5Rc1KjfycSfA9DMa6m+4C4eguHhw==} + engines: {node: '>=0.10.0'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + css-tree@3.1.0: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -1184,6 +1331,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deep-strict-equal@0.2.0: + resolution: {integrity: sha512-3daSWyvZ/zwJvuMGlzG1O+Ow0YSadGfb3jsh9xoCutv2tWyB9dA4YvR9L9/fSdDZa2dByYQe+TqapSGUrjnkoA==} + engines: {node: '>=0.10.0'} + draco3d@1.5.7: resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} @@ -1282,6 +1433,10 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1291,6 +1446,9 @@ packages: picomatch: optional: true + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -1347,6 +1505,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + h3-js@4.4.0: + resolution: {integrity: sha512-DvJh07MhGgY2KcC4OeZc8SSyA+ZXpdvoh6uCzGpoKvWtZxJB+g6VXXC1+eWYkaMIsLz7J/ErhOalHCpcs1KYog==} + engines: {node: '>=4', npm: '>=3', yarn: '>=1.3.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1367,6 +1529,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1380,6 +1545,9 @@ packages: engines: {node: '>=6.9.0'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1388,6 +1556,15 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-error@2.2.2: + resolution: {integrity: sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==} + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -1411,6 +1588,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1463,6 +1643,9 @@ packages: engines: {node: '>=6'} hasBin: true + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + kdbush@4.0.2: resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} @@ -1479,6 +1662,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1497,6 +1683,13 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + long@3.2.0: + resolution: {integrity: sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==} + engines: {node: '>=0.6'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@11.2.4: resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} @@ -1504,6 +1697,12 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lz4js@0.2.0: + resolution: {integrity: sha512-gY2Ia9Lm7Ep8qMiuGRhvUq0Q7qUereeldZPP1PMEJxPtEWHJLqw9pgX68oHajBH0nzJK4MaZEA/YNV3jT8u8Bg==} + + lzo-wasm@0.0.4: + resolution: {integrity: sha512-VKlnoJRFrB8SdJhlVKvW5vI1gGwcZ+mvChEXcSX6r2xDNc/Q2FD9esfBmGCuPZdrJ1feO+YcVFd2PTk0c137Gw==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1511,6 +1710,9 @@ packages: resolution: {integrity: sha512-O2ok6N/bQ9NA9nJ22r/PRQQYkUe9JwfDMjBPkQ+8OwsVH4TpA5skIAM2wc0k+rni5lVbAVONVyBvgi1rF2vEPA==} engines: {node: '>=16.14.0', npm: '>=8.1.0'} + md5@2.3.0: + resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -1582,6 +1784,9 @@ packages: package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -1606,6 +1811,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + pbf@4.0.1: resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} hasBin: true @@ -1658,6 +1867,9 @@ packages: engines: {node: '>=14'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + proj4@2.20.2: resolution: {integrity: sha512-ipfBRfQly0HhHTO7hnC1GfaX8bvroO7VV4KH889ehmADSE8C/qzp2j+Jj6783S9Tj6c2qX/hhYm7oH0kgXzBAA==} @@ -1706,6 +1918,9 @@ packages: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1740,6 +1955,9 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -1763,6 +1981,9 @@ packages: resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} engines: {node: '>=0.10.0'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1774,6 +1995,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + snappyjs@0.6.1: + resolution: {integrity: sha512-YIK6I2lsH072UE0aOFxxY1dPDCS43I5ktqHpeAsuLNYWkE5pGxRGWfDM4/vSUfNzXjC1Ivzt3qx31PCLmc9yqg==} + sort-asc@0.2.0: resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} engines: {node: '>=0.10.0'} @@ -1807,10 +2031,16 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1943,6 +2173,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uzip-module@1.0.3: resolution: {integrity: sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==} @@ -2095,6 +2328,9 @@ packages: zarrita@0.5.4: resolution: {integrity: sha512-i88iN2+HqIQ+uiCEWLfhjbYNXAJD7IrM4h3lFwFclfqEOOhxp10amRWtqmgN5jbuy3+h0LwdyLVVzk4y9rTLgg==} + zstd-codec@0.1.5: + resolution: {integrity: sha512-v3fyjpK8S/dpY/X5WxqTK3IoCnp/ZOLxn144GZVlNUjtwAchzrVo03h+oMATFhCIiJ5KTr4V3vDQQYz4RU684g==} + zstddec@0.1.0: resolution: {integrity: sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg==} @@ -2276,6 +2512,44 @@ snapshots: gl-matrix: 3.4.4 mjolnir.js: 3.0.0 + '@deck.gl/extensions@9.2.5(@deck.gl/core@9.2.5)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))': + dependencies: + '@deck.gl/core': 9.2.5 + '@luma.gl/constants': 9.2.4 + '@luma.gl/core': 9.2.4 + '@luma.gl/engine': 9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)) + '@luma.gl/shadertools': 9.2.4(@luma.gl/core@9.2.4) + '@math.gl/core': 4.1.0 + + '@deck.gl/geo-layers@9.2.5(@deck.gl/core@9.2.5)(@deck.gl/extensions@9.2.5(@deck.gl/core@9.2.5)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))))(@deck.gl/layers@9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))))(@deck.gl/mesh-layers@9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/gltf@9.2.4(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@loaders.gl/core@4.3.4)(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))': + dependencies: + '@deck.gl/core': 9.2.5 + '@deck.gl/extensions': 9.2.5(@deck.gl/core@9.2.5)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))) + '@deck.gl/layers': 9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4))) + '@deck.gl/mesh-layers': 9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/gltf@9.2.4(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)) + '@loaders.gl/3d-tiles': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/core': 4.3.4 + '@loaders.gl/gis': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/mvt': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/terrain': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/tiles': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/wms': 4.3.4(@loaders.gl/core@4.3.4) + '@luma.gl/core': 9.2.4 + '@luma.gl/engine': 9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)) + '@luma.gl/gltf': 9.2.4(@luma.gl/constants@9.2.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)) + '@luma.gl/shadertools': 9.2.4(@luma.gl/core@9.2.4) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + '@types/geojson': 7946.0.16 + a5-js: 0.5.0 + h3-js: 4.4.0 + long: 3.2.0 + transitivePeerDependencies: + - '@luma.gl/constants' + '@deck.gl/layers@9.2.5(@deck.gl/core@9.2.5)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.4)(@luma.gl/engine@9.2.4(@luma.gl/core@9.2.4)(@luma.gl/shadertools@9.2.4(@luma.gl/core@9.2.4)))': dependencies: '@deck.gl/core': 9.2.5 @@ -2464,6 +2738,40 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@loaders.gl/3d-tiles@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/compression': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/core': 4.3.4 + '@loaders.gl/crypto': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/draco': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/gltf': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/images': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/math': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/tiles': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/zip': 4.3.4(@loaders.gl/core@4.3.4) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/geospatial': 4.1.0 + '@probe.gl/log': 4.1.0 + long: 5.3.2 + + '@loaders.gl/compression@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/worker-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@types/brotli': 1.3.4 + '@types/pako': 1.0.7 + fflate: 0.7.4 + lzo-wasm: 0.0.4 + pako: 1.0.11 + snappyjs: 0.6.1 + optionalDependencies: + brotli: 1.3.3 + lz4js: 0.2.0 + zstd-codec: 0.1.5 + '@loaders.gl/core@4.3.4': dependencies: '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) @@ -2471,6 +2779,13 @@ snapshots: '@loaders.gl/worker-utils': 4.3.4(@loaders.gl/core@4.3.4) '@probe.gl/log': 4.1.0 + '@loaders.gl/crypto@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/worker-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@types/crypto-js': 4.2.2 + '@loaders.gl/draco@4.3.4(@loaders.gl/core@4.3.4)': dependencies: '@loaders.gl/core': 4.3.4 @@ -2479,6 +2794,15 @@ snapshots: '@loaders.gl/worker-utils': 4.3.4(@loaders.gl/core@4.3.4) draco3d: 1.5.7 + '@loaders.gl/gis@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + '@mapbox/vector-tile': 1.3.1 + '@math.gl/polygon': 4.1.0 + pbf: 3.3.0 + '@loaders.gl/gltf@4.3.4(@loaders.gl/core@4.3.4)': dependencies: '@loaders.gl/core': 4.3.4 @@ -2502,11 +2826,37 @@ snapshots: '@probe.gl/log': 4.1.0 '@probe.gl/stats': 4.1.0 + '@loaders.gl/math@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/images': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@math.gl/core': 4.1.0 + + '@loaders.gl/mvt@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/gis': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/images': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + '@math.gl/polygon': 4.1.0 + '@probe.gl/stats': 4.1.0 + pbf: 3.3.0 + '@loaders.gl/schema@4.3.4(@loaders.gl/core@4.3.4)': dependencies: '@loaders.gl/core': 4.3.4 '@types/geojson': 7946.0.16 + '@loaders.gl/terrain@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/images': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + '@mapbox/martini': 0.2.0 + '@loaders.gl/textures@4.3.4(@loaders.gl/core@4.3.4)': dependencies: '@loaders.gl/core': 4.3.4 @@ -2518,10 +2868,47 @@ snapshots: ktx-parse: 0.7.1 texture-compressor: 1.0.2 + '@loaders.gl/tiles@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/math': 4.3.4(@loaders.gl/core@4.3.4) + '@math.gl/core': 4.1.0 + '@math.gl/culling': 4.1.0 + '@math.gl/geospatial': 4.1.0 + '@math.gl/web-mercator': 4.1.0 + '@probe.gl/stats': 4.1.0 + + '@loaders.gl/wms@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/images': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/xml': 4.3.4(@loaders.gl/core@4.3.4) + '@turf/rewind': 5.1.5 + deep-strict-equal: 0.2.0 + '@loaders.gl/worker-utils@4.3.4(@loaders.gl/core@4.3.4)': dependencies: '@loaders.gl/core': 4.3.4 + '@loaders.gl/xml@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/core': 4.3.4 + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/schema': 4.3.4(@loaders.gl/core@4.3.4) + fast-xml-parser: 4.5.3 + + '@loaders.gl/zip@4.3.4(@loaders.gl/core@4.3.4)': + dependencies: + '@loaders.gl/compression': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/core': 4.3.4 + '@loaders.gl/crypto': 4.3.4(@loaders.gl/core@4.3.4) + '@loaders.gl/loader-utils': 4.3.4(@loaders.gl/core@4.3.4) + jszip: 3.10.1 + md5: 2.3.0 + '@luma.gl/constants@9.2.4': {} '@luma.gl/core@9.2.4': @@ -2573,12 +2960,20 @@ snapshots: '@mapbox/jsonlint-lines-primitives@2.0.2': {} + '@mapbox/martini@0.2.0': {} + + '@mapbox/point-geometry@0.1.0': {} + '@mapbox/point-geometry@1.1.0': {} '@mapbox/tiny-sdf@2.0.7': {} '@mapbox/unitbezier@0.0.1': {} + '@mapbox/vector-tile@1.3.1': + dependencies: + '@mapbox/point-geometry': 0.1.0 + '@mapbox/vector-tile@2.0.4': dependencies: '@mapbox/point-geometry': 1.1.0 @@ -2624,6 +3019,16 @@ snapshots: dependencies: '@math.gl/types': 4.1.0 + '@math.gl/culling@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + + '@math.gl/geospatial@4.1.0': + dependencies: + '@math.gl/core': 4.1.0 + '@math.gl/types': 4.1.0 + '@math.gl/polygon@4.1.0': dependencies: '@math.gl/core': 4.1.0 @@ -2718,6 +3123,33 @@ snapshots: '@standard-schema/spec@1.0.0': {} + '@turf/boolean-clockwise@5.1.5': + dependencies: + '@turf/helpers': 5.1.5 + '@turf/invariant': 5.2.0 + + '@turf/clone@5.1.5': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/helpers@5.1.5': {} + + '@turf/invariant@5.2.0': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/meta@5.2.0': + dependencies: + '@turf/helpers': 5.1.5 + + '@turf/rewind@5.1.5': + dependencies: + '@turf/boolean-clockwise': 5.1.5 + '@turf/clone': 5.1.5 + '@turf/helpers': 5.1.5 + '@turf/invariant': 5.2.0 + '@turf/meta': 5.2.0 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -2739,11 +3171,17 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/brotli@1.3.4': + dependencies: + '@types/node': 25.0.1 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -2762,6 +3200,8 @@ snapshots: '@types/offscreencanvas@2019.7.3': {} + '@types/pako@1.0.7': {} + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -2934,6 +3374,10 @@ snapshots: reference-spec-reader: 0.2.0 unzipit: 1.4.3 + a5-js@0.5.0: + dependencies: + gl-matrix: 3.4.4 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2969,6 +3413,9 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: + optional: true + baseline-browser-mapping@2.9.7: {} bidi-js@1.0.3: @@ -2984,6 +3431,11 @@ snapshots: dependencies: balanced-match: 1.0.2 + brotli@1.3.3: + dependencies: + base64-js: 1.5.1 + optional: true + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.7 @@ -2992,6 +3444,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.2(browserslist@4.28.1) + buf-compare@1.0.1: {} + bundle-require@5.1.0(esbuild@0.27.1): dependencies: esbuild: 0.27.1 @@ -3019,6 +3473,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + charenc@0.0.2: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -3039,12 +3495,21 @@ snapshots: convert-source-map@2.0.0: {} + core-assert@0.2.1: + dependencies: + buf-compare: 1.0.1 + is-error: 2.2.2 + + core-util-is@1.0.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + css-tree@3.1.0: dependencies: mdn-data: 2.12.2 @@ -3073,6 +3538,10 @@ snapshots: deep-is@0.1.4: {} + deep-strict-equal@0.2.0: + dependencies: + core-assert: 0.2.1 + draco3d@1.5.7: {} earcut@2.2.4: {} @@ -3205,10 +3674,16 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.2 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + fflate@0.7.4: {} + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -3263,6 +3738,8 @@ snapshots: globals@14.0.0: {} + h3-js@4.4.0: {} + has-flag@4.0.0: {} html-encoding-sniffer@4.0.0: @@ -3287,12 +3764,16 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} image-size@0.7.5: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -3300,6 +3781,12 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + + is-buffer@1.1.6: {} + + is-error@2.2.2: {} + is-extendable@0.1.1: {} is-extendable@1.0.1: @@ -3318,6 +3805,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + isarray@1.0.0: {} + isexe@2.0.0: {} isobject@3.0.1: {} @@ -3372,6 +3861,13 @@ snapshots: json5@2.2.3: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + kdbush@4.0.2: {} keyv@4.5.4: @@ -3387,6 +3883,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -3399,12 +3899,21 @@ snapshots: lodash.merge@4.6.2: {} + long@3.2.0: {} + + long@5.3.2: {} + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + lz4js@0.2.0: + optional: true + + lzo-wasm@0.0.4: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3435,6 +3944,12 @@ snapshots: supercluster: 8.0.1 tinyqueue: 3.0.0 + md5@2.3.0: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: 1.1.6 + mdn-data@2.12.2: {} mgrs@1.0.0: {} @@ -3503,6 +4018,8 @@ snapshots: package-manager-detector@1.6.0: {} + pako@1.0.11: {} + pako@2.1.0: {} parent-module@1.0.1: @@ -3521,6 +4038,11 @@ snapshots: pathe@2.0.3: {} + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + pbf@4.0.1: dependencies: resolve-protobuf-schema: 2.1.0 @@ -3555,6 +4077,8 @@ snapshots: prettier@3.7.4: {} + process-nextick-args@2.0.1: {} + proj4@2.20.2: dependencies: mgrs: 1.0.0 @@ -3593,6 +4117,16 @@ snapshots: react@19.2.3: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} reference-spec-reader@0.2.0: {} @@ -3641,6 +4175,8 @@ snapshots: dependencies: mri: 1.2.0 + safe-buffer@5.1.2: {} + safer-buffer@2.1.2: {} saxes@6.0.0: @@ -3660,6 +4196,8 @@ snapshots: is-plain-object: 2.0.4 split-string: 3.1.0 + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3668,6 +4206,8 @@ snapshots: siginfo@2.0.0: {} + snappyjs@0.6.1: {} + sort-asc@0.2.0: {} sort-desc@0.2.0: {} @@ -3695,8 +4235,14 @@ snapshots: std-env@3.10.0: {} + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + strip-json-comments@3.1.1: {} + strnum@1.1.2: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -3832,6 +4378,8 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + uzip-module@1.0.3: {} vite@7.3.0(@types/node@25.0.1): @@ -3935,4 +4483,7 @@ snapshots: '@zarrita/storage': 0.1.3 numcodecs: 0.3.2 + zstd-codec@0.1.5: + optional: true + zstddec@0.1.0: {} From 4ab9f27a3b5dedfa066da69bc8c0bd0a8cb4681f Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 15:59:08 -0500 Subject: [PATCH 02/12] Rename to RasterTileNode --- .../raster-tileset/raster-tile-traversal.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts index 92e583d7..3b465102 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts @@ -78,7 +78,8 @@ const REF_POINTS_9 = REF_POINTS_5.concat([ ]); /** - * COG Tile Node - similar to OSMNode but for COG's tile structure. + * Raster Tile Node - similar to OSMNode but for a generic raster tileset's + * tile structure. * * Represents a single tile in the COG internal tiling pyramid. * @@ -88,7 +89,7 @@ const REF_POINTS_9 = REF_POINTS_5.concat([ * - y: tile row (0 to COGOverview.tilesY, top to bottom) * - z: overview level. This uses TileMatrixSet ordering where: 0 = coarsest, higher = finer */ -export class COGTileNode { +export class RasterTileNode { /** Index across a row */ x: number; /** Index down a column */ @@ -114,7 +115,7 @@ export class COGTileNode { private selected?: boolean; /** A cache of the children of this node. */ - private _children?: COGTileNode[]; + private _children?: RasterTileNode[]; constructor(x: number, y: number, z: number, cogMetadata: COGMetadata) { this.x = x; @@ -129,7 +130,7 @@ export class COGTileNode { } /** Get the children of this node. */ - get children(): COGTileNode[] { + get children(): RasterTileNode[] { if (!this._children) { const maxZ = this.cogMetadata.overviews.length - 1; if (this.z >= maxZ) { @@ -158,7 +159,7 @@ export class COGTileNode { // resolutions (higher map zoom level) if (childX < childOverview.tilesX && childY < childOverview.tilesY) { this._children.push( - new COGTileNode(childX, childY, childZ, this.cogMetadata), + new RasterTileNode(childX, childY, childZ, this.cogMetadata), ); } } @@ -300,7 +301,7 @@ export class COGTileNode { * @param result - Accumulator array for selected tiles * @returns Array of selected OSMNode tiles */ - getSelected(result: COGTileNode[] = []): COGTileNode[] { + getSelected(result: RasterTileNode[] = []): RasterTileNode[] { if (this.selected) { result.push(this); } @@ -498,10 +499,10 @@ export function getTileIndices( // Create root tiles at coarsest level // In contrary to OSM tiling, we might have more than one tile at the // coarsest level (z=0) - const roots: COGTileNode[] = []; + const roots: RasterTileNode[] = []; for (let y = 0; y < coarsestOverview.tilesY; y++) { for (let x = 0; x < coarsestOverview.tilesX; x++) { - roots.push(new COGTileNode(x, y, 0, cogMetadata)); + roots.push(new RasterTileNode(x, y, 0, cogMetadata)); } } @@ -522,7 +523,7 @@ export function getTileIndices( console.log("roots", roots); // Collect selected tiles - const selectedNodes: COGTileNode[] = []; + const selectedNodes: RasterTileNode[] = []; for (const root of roots) { root.getSelected(selectedNodes); } From 409eff8e0a71ba01be7c8dc8e735928ed6ef174e Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 16:01:55 -0500 Subject: [PATCH 03/12] Rename `cogMetadata` to `metadata`, and COGMetadata to RasterTilesetMetadata --- .../raster-tileset/raster-tile-traversal.ts | 41 +++++++++++-------- .../src/raster-tileset/types.ts | 7 ++-- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts index 3b465102..82522509 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts @@ -27,7 +27,7 @@ import { } from "@math.gl/culling"; import type { - COGMetadata, + RasterTilesetMetadata, COGOverview, COGTileIndex, ZRange, @@ -97,7 +97,7 @@ export class RasterTileNode { /** TileMatrixSet-style zoom index (higher = finer detail) */ z: number; - private cogMetadata: COGMetadata; + private metadata: RasterTilesetMetadata; /** * Flag indicating whether any descendant of this tile is visible. @@ -117,22 +117,27 @@ export class RasterTileNode { /** A cache of the children of this node. */ private _children?: RasterTileNode[]; - constructor(x: number, y: number, z: number, cogMetadata: COGMetadata) { + constructor( + x: number, + y: number, + z: number, + metadata: RasterTilesetMetadata, + ) { this.x = x; this.y = y; this.z = z; - this.cogMetadata = cogMetadata; + this.metadata = metadata; } /** Get overview info for this tile's z level */ get overview(): COGOverview { - return this.cogMetadata.overviews[this.z]; + return this.metadata.overviews[this.z]!; } /** Get the children of this node. */ get children(): RasterTileNode[] { if (!this._children) { - const maxZ = this.cogMetadata.overviews.length - 1; + const maxZ = this.metadata.overviews.length - 1; if (this.z >= maxZ) { // Already at finest resolution, no children return []; @@ -141,7 +146,7 @@ export class RasterTileNode { // In TileMatrixSet ordering: refine to z + 1 (finer detail) const childZ = this.z + 1; const parentOverview = this.overview; - const childOverview = this.cogMetadata.overviews[childZ]; + const childOverview = this.metadata.overviews[childZ]; // Calculate scale factor between levels const scaleFactor = @@ -159,7 +164,7 @@ export class RasterTileNode { // resolutions (higher map zoom level) if (childX < childOverview.tilesX && childY < childOverview.tilesY) { this._children.push( - new RasterTileNode(childX, childY, childZ, this.cogMetadata), + new RasterTileNode(childX, childY, childZ, this.metadata), ); } } @@ -187,7 +192,7 @@ export class RasterTileNode { cullingVolume, elevationBounds, minZ, - maxZ = this.cogMetadata.overviews.length - 1, + maxZ = this.metadata.overviews.length - 1, project, } = params; @@ -323,7 +328,7 @@ export class RasterTileNode { project: ((xyz: number[]) => number[]) | null, ) { const overview = this.overview; - const { tileWidth, tileHeight } = this.cogMetadata; + const { tileWidth, tileHeight } = this.metadata; // Use geotransform to calculate tile bounds // geotransform: [a, b, c, d, e, f] where: @@ -375,11 +380,11 @@ export class RasterTileNode { for (const [pX, pY] of refPointPositionsImage) { // Reproject to Web Mercator (EPSG 3857) - const projected = this.cogMetadata.projectTo3857.forward([pX, pY]); + const projected = this.metadata.projectTo3857.forward([pX, pY]); refPointPositionsProjected.push(projected); // Also log WGS84 for comparison - const wgs84 = this.cogMetadata.projectToWgs84.forward([pX, pY]); + const wgs84 = this.metadata.projectToWgs84.forward([pX, pY]); console.log( `Image [${pX.toFixed(2)}, ${pY.toFixed(2)}] -> WGS84 [${wgs84[0].toFixed(6)}, ${wgs84[1].toFixed(6)}] -> WebMerc [${projected[0].toFixed(2)}, ${projected[1].toFixed(2)}]`, ); @@ -444,7 +449,7 @@ export class RasterTileNode { * This is a placeholder - needs proper projection library (proj4js) */ private cogCoordsToLngLat([x, y]: [number, number]): number[] { - const [lng, lat] = this.cogMetadata.projectToWgs84.forward([x, y]); + const [lng, lat] = this.metadata.projectToWgs84.forward([x, y]); return [lng, lat, 0]; } } @@ -456,7 +461,7 @@ export class RasterTileNode { * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer */ export function getTileIndices( - cogMetadata: COGMetadata, + metadata: COGMetadata, opts: { viewport: Viewport; maxZ: number; @@ -468,8 +473,8 @@ export function getTileIndices( // console.log("=== getTileIndices called ==="); // console.log("Viewport:", viewport); // console.log("maxZ:", maxZ); - // console.log("COG metadata overviews count:", cogMetadata.overviews.length); - // console.log("COG bbox:", cogMetadata.bbox); + // console.log("COG metadata overviews count:", metadata.overviews.length); + // console.log("COG bbox:", metadata.bbox); const project: ((xyz: number[]) => number[]) | null = viewport instanceof _GlobeViewport && viewport.resolution @@ -494,7 +499,7 @@ export function getTileIndices( viewport instanceof WebMercatorViewport && viewport.pitch <= 60 ? maxZ : 0; // Start from coarsest overview - const coarsestOverview = cogMetadata.overviews[0]; + const coarsestOverview = metadata.overviews[0]; // Create root tiles at coarsest level // In contrary to OSM tiling, we might have more than one tile at the @@ -502,7 +507,7 @@ export function getTileIndices( const roots: RasterTileNode[] = []; for (let y = 0; y < coarsestOverview.tilesY; y++) { for (let x = 0; x < coarsestOverview.tilesX; x++) { - roots.push(new RasterTileNode(x, y, 0, cogMetadata)); + roots.push(new RasterTileNode(x, y, 0, metadata)); } } diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts index 75a3156e..fadf838a 100644 --- a/packages/deck.gl-raster/src/raster-tileset/types.ts +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -181,7 +181,7 @@ export type COGOverview = { /** * COG Metadata extracted from GeoTIFF */ -export type COGMetadata = { +export type RasterTilesetMetadata = { width: number; height: number; /** Number of pixels wide for each tile */ @@ -192,9 +192,8 @@ export type COGMetadata = { bbox: Bounds; // COG's CRS projection: string | null; overviews: COGOverview[]; - image: GeoTIFF; // GeoTIFF reference - projectToWgs84: Converter; - projectTo3857: Converter; + projectToWgs84: (x: number, y: number) => [number, number]; + projectTo3857: (x: number, y: number) => [number, number]; }; /** From d12fc8522e533f8e81fef810d3ae024da2b6b595 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 16:36:39 -0500 Subject: [PATCH 04/12] clean up types defined in types.ts --- .../src/raster-tileset/types.ts | 300 ++++++------------ 1 file changed, 89 insertions(+), 211 deletions(-) diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts index fadf838a..d222a205 100644 --- a/packages/deck.gl-raster/src/raster-tileset/types.ts +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -30,12 +30,21 @@ export type TileLoadProps = { zoom?: number; }; -//////////////// -// Claude-generated metadata -//////////////// +export type Point = [number, number]; + +type CRS = any; + +/** + * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere + */ +export interface TileMatrixSetBoundingBox { + lowerLeft: Point; + upperRight: Point; + crs?: CRS; +} /** - * Represents a single resolution level in a Cloud Optimized GeoTIFF. + * Represents a single resolution level in a raster tileset. * * COGs contain multiple resolution levels (overviews) for efficient * visualization at different zoom levels. @@ -46,47 +55,51 @@ export type TileLoadProps = { * * This matches the natural ordering where z increases with detail. */ -export type COGOverview = { +export type TileMatrix = { /** - * Overview index in the TileMatrixSet ordering. - * - Index 0: Coarsest resolution (most zoomed out) - * - Higher indices: Progressively finer resolution - * - * This is the index in the COGMetadata.overviews array and represents - * the natural ordering from coarse to fine. - * - * Note: This is different from GeoTIFF's internal level numbering where - * level 0 is the full resolution image. + * Scale denominator of this tile matrix. * - * @example - * // For a COG with 4 resolutions: - * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) - * index: 1 // Medium: 2500x2000 pixels (4x downsampled) - * index: 2 // Fine: 5000x4000 pixels (2x downsampled) - * index: 3 // Finest: 10000x8000 pixels (full resolution) + * Defined as cellSize (meters per pixel) * meters per unit / 0.00028 */ - level: number; + scaleDenominator: number; /** - * Zoom index (OSM convention). - * Defined as: maxLevel - currentLevel + * Cell size of this tile matrix. * - * This makes the code compatible with OSM tile indexing where: - * - Higher z = finer detail (opposite of COG level) - * - Lower z = coarser detail - * - * In TileMatrixSet ordering: z === level (both increase with detail) + * This is the pixel size in meters. */ - z: number; + cellSize: number; - /** - * Width of the entire image at this overview level, in pixels. + // /** + // * Overview index in the TileMatrixSet ordering. + // * - Index 0: Coarsest resolution (most zoomed out) + // * - Higher indices: Progressively finer resolution + // * + // * This is the index in the COGMetadata.overviews array and represents + // * the natural ordering from coarse to fine. + // * + // * Note: This is different from GeoTIFF's internal level numbering where + // * level 0 is the full resolution image. + // * + // * @example + // * // For a COG with 4 resolutions: + // * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) + // * index: 1 // Medium: 2500x2000 pixels (4x downsampled) + // * index: 2 // Fine: 5000x4000 pixels (2x downsampled) + // * index: 3 // Finest: 10000x8000 pixels (full resolution) + // */ + // z: number; + + pointOfOrigin: Point; + + /** + * Width of each tile of this tile matrix in pixels. */ - width: number; + tileWidth: number; /** - * Height of the entire image at this overview level, in pixels. + * Height of each tile of this tile matrix in pixels. */ - height: number; + tileHeight: number; /** * Number of tiles in the X (horizontal) direction at this overview level. @@ -100,7 +113,7 @@ export type COGOverview = { * tilesX: 10 // z=2: ceil(5000 / 512) * tilesX: 20 // z=3: ceil(10000 / 512) */ - tilesX: number; + matrixWidth: number; /** * Number of tiles in the Y (vertical) direction at this overview level. @@ -114,44 +127,27 @@ export type COGOverview = { * tilesY: 8 // z=2: ceil(4000 / 512) * tilesY: 16 // z=3: ceil(8000 / 512) */ - tilesY: number; + matrixHeight: number; - /** - * Downsampling scale factor relative to full resolution (finest level). - * - * Indicates how much this overview is downsampled compared to the finest resolution. - * - Scale factor of 1: Full resolution (finest level) - * - Scale factor of 2: Half resolution - * - Scale factor of 4: Quarter resolution - * - Scale factor of 8: Eighth resolution (coarsest in this example) - * - * Common pattern: Each overview is 2x downsampled from the next finer level, - * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) - * - * @example - * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) - * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) - * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) - * scaleFactor: 1 // z=3: 10000x8000 (full resolution) - */ - scaleFactor: number; - - /** - * Index in the original GeoTIFF file. - * - * GeoTIFF stores: image 0 = full resolution, image 1+ = overviews (progressively coarser) - * This index is needed to read the correct image from the GeoTIFF file. - * - * Note: This may differ from `level` since we reorder overviews to TileMatrixSet order. - * - * @example - * // TileMatrixSet order (our array): - * level: 0, geoTiffIndex: 3 // Coarsest (GeoTIFF overview 3) - * level: 1, geoTiffIndex: 2 // Medium (GeoTIFF overview 2) - * level: 2, geoTiffIndex: 1 // Fine (GeoTIFF overview 1) - * level: 3, geoTiffIndex: 0 // Finest (GeoTIFF main image) - */ - geoTiffIndex: number; + // /** + // * Downsampling scale factor relative to full resolution (finest level). + // * + // * Indicates how much this overview is downsampled compared to the finest resolution. + // * - Scale factor of 1: Full resolution (finest level) + // * - Scale factor of 2: Half resolution + // * - Scale factor of 4: Quarter resolution + // * - Scale factor of 8: Eighth resolution (coarsest in this example) + // * + // * Common pattern: Each overview is 2x downsampled from the next finer level, + // * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) + // * + // * @example + // * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) + // * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) + // * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) + // * scaleFactor: 1 // z=3: 10000x8000 (full resolution) + // */ + // scaleFactor: number; /** * Affine geotransform for this overview level. @@ -181,66 +177,7 @@ export type COGOverview = { /** * COG Metadata extracted from GeoTIFF */ -export type RasterTilesetMetadata = { - width: number; - height: number; - /** Number of pixels wide for each tile */ - tileWidth: number; - tileHeight: number; - tilesX: number; - tilesY: number; - bbox: Bounds; // COG's CRS - projection: string | null; - overviews: COGOverview[]; - projectToWgs84: (x: number, y: number) => [number, number]; - projectTo3857: (x: number, y: number) => [number, number]; -}; - -/** - * COG Tile Index - * - * In TileMatrixSet ordering: level === z (both 0 = coarsest, higher = finer) - */ -export type COGTileIndex = { - x: number; - y: number; - z: number; // TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) -}; - -//////////////// -// TileMatrixSet -//////////////// - -// type CRS = string | { [k: string]: unknown }; -export type TMSCrs = unknown; - -/** - * A 2D Point in the CRS indicated elsewhere - * - * @minItems 2 - * @maxItems 2 - */ -export type TMSPoint = [number, number]; - -/** - * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere - */ -export interface TMSBoundingBox { - lowerLeft: TMSPoint; - upperRight: TMSPoint; - crs?: TMSCrs; - /** - * @minItems 2 - * @maxItems 2 - */ - orderedAxes?: [string, string]; - [k: string]: unknown; -} - -/** - * A definition of a tile matrix set following the Tile Matrix Set standard. For tileset metadata, such a description (in `tileMatrixSet` property) is only required for offline use, as an alternative to a link with a `http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme` relation type. - */ -export type TileMatrixSetDefinition = { +export type TileMatrixSet = { /** * Title of this tile matrix set, normally used for display to a human */ @@ -249,91 +186,32 @@ export type TileMatrixSetDefinition = { * Brief narrative description of this tile matrix set, normally available for display to a human */ description?: string; - /** - * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this tile matrix set - */ - keywords?: string[]; - /** - * Tile matrix set identifier. Implementation of 'identifier' - */ - id?: string; - /** - * Reference to an official source for this tileMatrixSet - */ - uri?: string; - /** - * @minItems 1 - */ - orderedAxes?: [string, ...string[]]; - crs: TMSCrs; - /** - * Reference to a well-known scale set - */ - wellKnownScaleSet?: string; - boundingBox?: { - [k: string]: unknown; - } & TMSBoundingBox; + crs: CRS; + + boundingBox?: TileMatrixSetBoundingBox; /** * Describes scale levels and its tile matrices */ - tileMatrices: TMSTileMatrix[]; - [k: string]: unknown; + tileMatrices: TileMatrix[]; + + projectToWgs84: (x: number, y: number) => [number, number]; + projectTo3857: (x: number, y: number) => [number, number]; }; /** - * A tile matrix, usually corresponding to a particular zoom level of a TileMatrixSet. + * Raster Tile Index + * + * In TileMatrixSet ordering: `level === z`. + * + * So level `z` is the coarsest resolution (0) and the highest `z` is the finest + * resolution. */ -export interface TMSTileMatrix { - /** - * Title of this tile matrix, normally used for display to a human - */ - title?: string; - /** - * Brief narrative description of this tile matrix set, normally available for display to a human - */ - description?: string; - /** - * Unordered list of one or more commonly used or formalized word(s) or phrase(s) used to describe this dataset - */ - keywords?: string[]; - /** - * Identifier selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile. Implementation of 'identifier' - */ - id: string; - /** - * Scale denominator of this tile matrix - */ - scaleDenominator: number; - /** - * Cell size of this tile matrix - */ - cellSize: number; - /** - * The corner of the tile matrix (_topLeft_ or _bottomLeft_) used as the origin for numbering tile rows and columns. This corner is also a corner of the (0, 0) tile. - */ - cornerOfOrigin?: "topLeft" | "bottomLeft"; - pointOfOrigin: { - [k: string]: unknown; - } & TMSPoint; - /** - * Width of each tile of this tile matrix in pixels - */ - tileWidth: number; - /** - * Height of each tile of this tile matrix in pixels - */ - tileHeight: number; - /** - * Width of the matrix (number of tiles in width) - */ - matrixHeight: number; - /** - * Height of the matrix (number of tiles in height) - */ - matrixWidth: number; +export type RasterTileIndex = { + x: number; + y: number; + /** - * Describes the rows that has variable matrix width + * TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) */ - variableMatrixWidths?: object[]; - [k: string]: unknown; -} + z: number; +}; From 0b4a6f696da6b1db39a29ba6814bfa6c426bdc3d Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 16:43:31 -0500 Subject: [PATCH 05/12] Update traversal for new type names --- .../raster-tileset/raster-tile-traversal.ts | 71 +++++++++---------- .../src/raster-tileset/types.ts | 8 +-- 2 files changed, 35 insertions(+), 44 deletions(-) diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts index 82522509..bb7a3ebb 100644 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts +++ b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts @@ -26,12 +26,7 @@ import { makeOrientedBoundingBoxFromPoints, } from "@math.gl/culling"; -import type { - RasterTilesetMetadata, - COGOverview, - COGTileIndex, - ZRange, -} from "./types.js"; +import type { TileMatrixSet, TileMatrix, TileIndex, ZRange } from "./types.js"; /** * The size of the entire world in deck.gl's common coordinate space. @@ -61,7 +56,7 @@ const WORLD_SIZE = 512; // will never be exact axis aligned boxes in Web Mercator space. // For most tiles: sample 4 corners and center (5 points total) -const REF_POINTS_5 = [ +const REF_POINTS_5: [number, number][] = [ [0.5, 0.5], // center [0, 0], // top-left [0, 1], // bottom-left @@ -85,8 +80,8 @@ const REF_POINTS_9 = REF_POINTS_5.concat([ * * COG tile nodes use the following coordinate system: * - * - x: tile column (0 to COGOverview.tilesX, left to right) - * - y: tile row (0 to COGOverview.tilesY, top to bottom) + * - x: tile column (0 to TileMatrix.tilesX, left to right) + * - y: tile row (0 to TileMatrix.tilesY, top to bottom) * - z: overview level. This uses TileMatrixSet ordering where: 0 = coarsest, higher = finer */ export class RasterTileNode { @@ -97,7 +92,7 @@ export class RasterTileNode { /** TileMatrixSet-style zoom index (higher = finer detail) */ z: number; - private metadata: RasterTilesetMetadata; + private metadata: TileMatrixSet; /** * Flag indicating whether any descendant of this tile is visible. @@ -117,12 +112,7 @@ export class RasterTileNode { /** A cache of the children of this node. */ private _children?: RasterTileNode[]; - constructor( - x: number, - y: number, - z: number, - metadata: RasterTilesetMetadata, - ) { + constructor(x: number, y: number, z: number, metadata: TileMatrixSet) { this.x = x; this.y = y; this.z = z; @@ -130,14 +120,14 @@ export class RasterTileNode { } /** Get overview info for this tile's z level */ - get overview(): COGOverview { - return this.metadata.overviews[this.z]!; + get overview(): TileMatrix { + return this.metadata.tileMatrices[this.z]!; } /** Get the children of this node. */ get children(): RasterTileNode[] { if (!this._children) { - const maxZ = this.metadata.overviews.length - 1; + const maxZ = this.metadata.tileMatrices.length - 1; if (this.z >= maxZ) { // Already at finest resolution, no children return []; @@ -146,11 +136,10 @@ export class RasterTileNode { // In TileMatrixSet ordering: refine to z + 1 (finer detail) const childZ = this.z + 1; const parentOverview = this.overview; - const childOverview = this.metadata.overviews[childZ]; + const childOverview = this.metadata.tileMatrices[childZ]!; // Calculate scale factor between levels - const scaleFactor = - parentOverview.scaleFactor / childOverview.scaleFactor; + const scaleFactor = parentOverview.cellSize / childOverview.cellSize; // Generate child tiles this._children = []; @@ -162,7 +151,10 @@ export class RasterTileNode { // Only create child if it's within bounds // Some tiles on the edges might not need to be created at higher // resolutions (higher map zoom level) - if (childX < childOverview.tilesX && childY < childOverview.tilesY) { + if ( + childX < childOverview.tileWidth && + childY < childOverview.tileHeight + ) { this._children.push( new RasterTileNode(childX, childY, childZ, this.metadata), ); @@ -192,7 +184,7 @@ export class RasterTileNode { cullingVolume, elevationBounds, minZ, - maxZ = this.metadata.overviews.length - 1, + maxZ = this.metadata.tileMatrices.length - 1, project, } = params; @@ -223,7 +215,7 @@ export class RasterTileNode { ); for (let i = 0; i < cullingVolume.planes.length; i++) { - const plane = cullingVolume.planes[i]; + const plane = cullingVolume.planes[i]!; const result = boundingVolume.intersectPlane(plane); const planeNames = ["left", "right", "bottom", "top", "near", "far"]; @@ -328,7 +320,8 @@ export class RasterTileNode { project: ((xyz: number[]) => number[]) | null, ) { const overview = this.overview; - const { tileWidth, tileHeight } = this.metadata; + const { tileMatrices } = this.metadata; + const { tileWidth, tileHeight } = tileMatrices[tileMatrices.length - 1]!; // Use geotransform to calculate tile bounds // geotransform: [a, b, c, d, e, f] where: @@ -348,7 +341,7 @@ export class RasterTileNode { console.log("refPoints", refPoints); /** Reference points positions in image CRS */ - const refPointPositionsImage: number[][] = []; + const refPointPositionsImage: [number, number][] = []; for (const [pX, pY] of refPoints) { // pX, pY are in [0, 1] range @@ -376,15 +369,15 @@ export class RasterTileNode { } /** Reference points positions in EPSG 3857 */ - const refPointPositionsProjected: number[][] = []; + const refPointPositionsProjected: [number, number][] = []; for (const [pX, pY] of refPointPositionsImage) { // Reproject to Web Mercator (EPSG 3857) - const projected = this.metadata.projectTo3857.forward([pX, pY]); + const projected = this.metadata.projectTo3857([pX, pY]); refPointPositionsProjected.push(projected); // Also log WGS84 for comparison - const wgs84 = this.metadata.projectToWgs84.forward([pX, pY]); + const wgs84 = this.metadata.projectToWgs84([pX, pY]); console.log( `Image [${pX.toFixed(2)}, ${pY.toFixed(2)}] -> WGS84 [${wgs84[0].toFixed(6)}, ${wgs84[1].toFixed(6)}] -> WebMerc [${projected[0].toFixed(2)}, ${projected[1].toFixed(2)}]`, ); @@ -401,7 +394,7 @@ export class RasterTileNode { const WEB_MERCATOR_MAX = 20037508.342789244; // Half Earth circumference /** Reference points positions in deck.gl world space */ - const refPointPositionsWorld: number[][] = []; + const refPointPositionsWorld: [number, number, number][] = []; for (const [mercX, mercY] of refPointPositionsProjected) { // X: offset from [-20M, 20M] to [0, 40M], then normalize to [0, 512] @@ -449,7 +442,7 @@ export class RasterTileNode { * This is a placeholder - needs proper projection library (proj4js) */ private cogCoordsToLngLat([x, y]: [number, number]): number[] { - const [lng, lat] = this.metadata.projectToWgs84.forward([x, y]); + const [lng, lat] = this.metadata.projectToWgs84([x, y]); return [lng, lat, 0]; } } @@ -461,19 +454,19 @@ export class RasterTileNode { * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer */ export function getTileIndices( - metadata: COGMetadata, + metadata: TileMatrixSet, opts: { viewport: Viewport; maxZ: number; zRange: ZRange | null; }, -): COGTileIndex[] { +): TileIndex[] { const { viewport, maxZ, zRange } = opts; // console.log("=== getTileIndices called ==="); // console.log("Viewport:", viewport); // console.log("maxZ:", maxZ); - // console.log("COG metadata overviews count:", metadata.overviews.length); + // console.log("COG metadata tileMatrices count:", metadata.tileMatrices.length); // console.log("COG bbox:", metadata.bbox); const project: ((xyz: number[]) => number[]) | null = @@ -488,7 +481,7 @@ export function getTileIndices( const cullingVolume = new CullingVolume(planes); // Project zRange from meters to common space - const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2]; + const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2]!; const elevationMin = (zRange && zRange[0] * unitsPerMeter) || 0; const elevationMax = (zRange && zRange[1] * unitsPerMeter) || 0; @@ -499,14 +492,14 @@ export function getTileIndices( viewport instanceof WebMercatorViewport && viewport.pitch <= 60 ? maxZ : 0; // Start from coarsest overview - const coarsestOverview = metadata.overviews[0]; + const coarsestOverview = metadata.tileMatrices[0]!; // Create root tiles at coarsest level // In contrary to OSM tiling, we might have more than one tile at the // coarsest level (z=0) const roots: RasterTileNode[] = []; - for (let y = 0; y < coarsestOverview.tilesY; y++) { - for (let x = 0; x < coarsestOverview.tilesX; x++) { + for (let y = 0; y < coarsestOverview.tileHeight; y++) { + for (let x = 0; x < coarsestOverview.tileWidth; x++) { roots.push(new RasterTileNode(x, y, 0, metadata)); } } diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts index d222a205..40b69b40 100644 --- a/packages/deck.gl-raster/src/raster-tileset/types.ts +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -17,8 +17,6 @@ export type NonGeoBoundingBox = { export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; -export type TileIndex = { x: number; y: number; z: number }; - export type TileLoadProps = { index: TileIndex; id: string; @@ -194,8 +192,8 @@ export type TileMatrixSet = { */ tileMatrices: TileMatrix[]; - projectToWgs84: (x: number, y: number) => [number, number]; - projectTo3857: (x: number, y: number) => [number, number]; + projectToWgs84: (point: [number, number]) => [number, number]; + projectTo3857: (point: [number, number]) => [number, number]; }; /** @@ -206,7 +204,7 @@ export type TileMatrixSet = { * So level `z` is the coarsest resolution (0) and the highest `z` is the finest * resolution. */ -export type RasterTileIndex = { +export type TileIndex = { x: number; y: number; From a2a875c788c5360db82b8185a9734b0f0c6154e5 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 18:49:18 -0500 Subject: [PATCH 06/12] Extract Tile Matrix Set from COG --- .../deck.gl-cog/src/cog-tile-matrix-set.ts | 153 ++++++++++++++++++ packages/deck.gl-cog/src/cog-tileset.ts | 0 .../deck.gl-cog/src/geotiff-reprojection.ts | 28 +++- packages/deck.gl-raster/src/index.ts | 1 + .../src/raster-tileset/types.ts | 2 + 5 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 packages/deck.gl-cog/src/cog-tile-matrix-set.ts delete mode 100644 packages/deck.gl-cog/src/cog-tileset.ts diff --git a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts new file mode 100644 index 00000000..bd46d710 --- /dev/null +++ b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts @@ -0,0 +1,153 @@ +import type { + TileMatrix, + TileMatrixSet, +} from "@developmentseed/deck.gl-raster"; +import type { GeoTIFF, GeoTIFFImage } from "geotiff"; +import { + extractGeotransform, + getGeoTIFFProjection, +} from "./geotiff-reprojection"; +import proj4 from "proj4"; + +// 0.28 mm per pixel +// https://docs.ogc.org/is/17-083r4/17-083r4.html#toc15 +const SCREEN_PIXEL_SIZE = 0.00028; + +/** + * + * Ported from Vincent's work here: + * https://github.com/developmentseed/morecantile/pull/187/changes#diff-402eedddfa30af554d03750c8a82a09962b44b044976c321b774b484b98e8f48R665 + * + * @return {TileMatrixSet}[return description] + */ +export async function parseCOGTileMatrixSet( + tiff: GeoTIFF, +): Promise { + const fullResImage = await tiff.getImage(); + const imageCount = await tiff.getImageCount(); + const bbox = fullResImage.getBoundingBox(); + const fullImageWidth = fullResImage.getWidth(); + const fullImageHeight = fullResImage.getHeight(); + + const crs = await getGeoTIFFProjection(fullResImage); + if (crs === null) { + throw new Error( + "Could not determine coordinate reference system from GeoTIFF geo keys", + ); + } + const projectToWgs84 = proj4(crs, "EPSG:4326").forward; + const projectTo3857 = proj4(crs, "EPSG:3857").forward; + + const boundingBox: TileMatrixSet["boundingBox"] = { + lowerLeft: [bbox[0]!, bbox[1]!], + upperRight: [bbox[2]!, bbox[3]!], + }; + + const transform = extractGeotransform(fullResImage); + const cellSize = Math.abs(transform[0]); + + const tileWidth = fullResImage.getTileWidth(); + const tileHeight = fullResImage.getTileHeight(); + + const tileMatrices: TileMatrix[] = [ + { + // Set as highest resolution / finest level + id: String(imageCount - 1), + scaleDenominator: (cellSize * metersPerUnit()) / SCREEN_PIXEL_SIZE, + cellSize, + pointOfOrigin: [transform[2], transform[5]], + tileWidth: fullResImage.getTileWidth(), + tileHeight: fullResImage.getTileHeight(), + matrixWidth: Math.ceil(fullImageWidth / tileWidth), + matrixHeight: Math.ceil(fullImageHeight / tileHeight), + geotransform: transform, + }, + ]; + + // Starting from 1 to skip full res image + for (let imageIdx = 1; imageIdx < imageCount - 1; imageIdx++) { + const image = await tiff.getImage(imageIdx); + const tileMatrix = createOverviewTileMatrix({ + id: String(imageCount - 1 - imageIdx), + image, + fullWidth: fullImageWidth, + fullHeight: fullImageHeight, + baseTransform: transform, + }); + tileMatrices.push(tileMatrix); + } + + // Reverse to have coarsest level first + tileMatrices.reverse(); + + return { + crs, + boundingBox, + tileMatrices, + projectToWgs84, + projectTo3857, + }; +} + +/** + * Coefficient to convert the coordinate reference system (CRS) + * units into meters (metersPerUnit). + * + * From note g in http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2: + * + * > If the CRS uses meters as units of measure for the horizontal dimensions, + * > then metersPerUnit=1; if it has degrees, then metersPerUnit=2pa/360 + * > (a is the Earth maximum radius of the ellipsoid). + */ +// https://github.com/developmentseed/morecantile/blob/7c95a11c491303700d6e33e9c1607f2719584dec/morecantile/utils.py#L67-L90 +function metersPerUnit(): number { + // For now, we assume our projection is in meters + return 1; +} + +/** + * Create tile matrix for COG overview + */ +function createOverviewTileMatrix({ + id, + image, + fullWidth, + baseTransform, +}: { + id: string; + image: GeoTIFFImage; + fullWidth: number; + fullHeight: number; + baseTransform: [number, number, number, number, number, number]; +}): TileMatrix { + const width = image.getWidth(); + const height = image.getHeight(); + + const scale = fullWidth / width; + // assert scale is same for x and y? + + const geotransform: [number, number, number, number, number, number] = [ + baseTransform[0] * scale, + baseTransform[1] * scale, + baseTransform[2], // x origin stays the same + baseTransform[3] * scale, + baseTransform[4] * scale, + baseTransform[5], // y origin stays the same + ]; + const cellSize = Math.abs(geotransform[0]); + + const tileWidth = image.getTileWidth(); + const tileHeight = image.getTileHeight(); + + return { + id, + scaleDenominator: (cellSize * metersPerUnit()) / SCREEN_PIXEL_SIZE, + cellSize, + pointOfOrigin: [geotransform[2], geotransform[5]], + tileWidth, + tileHeight, + matrixWidth: Math.ceil(width / tileWidth), + matrixHeight: Math.ceil(height / tileHeight), + geotransform, + }; +} diff --git a/packages/deck.gl-cog/src/cog-tileset.ts b/packages/deck.gl-cog/src/cog-tileset.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/deck.gl-cog/src/geotiff-reprojection.ts b/packages/deck.gl-cog/src/geotiff-reprojection.ts index ec914c2e..d6ac123c 100644 --- a/packages/deck.gl-cog/src/geotiff-reprojection.ts +++ b/packages/deck.gl-cog/src/geotiff-reprojection.ts @@ -93,15 +93,11 @@ export async function extractGeotiffReprojectors( ): Promise { const image = await tiff.getImage(); - const geoKeys = image.getGeoKeys(); - const projectionCode: number | null = - geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || null; - // Extract geotransform from full-resolution image // Only the top-level IFD has geo keys, so we'll derive overviews from this const baseGeotransform = extractGeotransform(image); - const sourceProjection = await getProjjson(projectionCode); + const sourceProjection = await getGeoTIFFProjection(image); if (sourceProjection === null) { throw new Error( "Could not determine source projection from GeoTIFF geo keys", @@ -135,6 +131,23 @@ export function fromGeoTransform( }; } +/** + * Get the Projection of a GeoTIFF + * + * The first `image` must be passed in, as only the top-level IFD contains geo + * keys. + */ +export async function getGeoTIFFProjection( + image: GeoTIFFImage, +): Promise { + const geoKeys = image.getGeoKeys(); + const projectionCode: number | null = + geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || null; + + const sourceProjection = await getProjjson(projectionCode); + return sourceProjection; +} + /** Query epsg.io for the PROJJSON corresponding to the given EPSG code. */ async function getProjjson(projectionCode: number | null) { if (projectionCode === null) { @@ -153,6 +166,9 @@ async function getProjjson(projectionCode: number | null) { /** * Extract affine geotransform from a GeoTIFF image. * + * The first `image` must be passed in, as only the top-level IFD contains geo + * keys. + * * Returns a 6-element array in Python `affine` package ordering: * [a, b, c, d, e, f] where: * - x_geo = a * col + b * row + c @@ -160,7 +176,7 @@ async function getProjjson(projectionCode: number | null) { * * This is NOT GDAL ordering, which is [c, a, b, f, d, e]. */ -function extractGeotransform( +export function extractGeotransform( image: GeoTIFFImage, ): [number, number, number, number, number, number] { const origin = image.getOrigin(); diff --git a/packages/deck.gl-raster/src/index.ts b/packages/deck.gl-raster/src/index.ts index 4ff386e5..47f84663 100644 --- a/packages/deck.gl-raster/src/index.ts +++ b/packages/deck.gl-raster/src/index.ts @@ -1,2 +1,3 @@ export { RasterLayer } from "./raster-layer.js"; export type { RasterLayerProps } from "./raster-layer.js"; +export type { TileMatrixSet, TileMatrix } from "./raster-tileset/types.js"; diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts index 40b69b40..d0efe214 100644 --- a/packages/deck.gl-raster/src/raster-tileset/types.ts +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -54,6 +54,8 @@ export interface TileMatrixSetBoundingBox { * This matches the natural ordering where z increases with detail. */ export type TileMatrix = { + id: string; + /** * Scale denominator of this tile matrix. * From cc042511b71586f1a6afb195812a047f14cd4ecc Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:02:00 -0500 Subject: [PATCH 07/12] fix iteration index --- packages/deck.gl-cog/src/cog-tile-matrix-set.ts | 2 +- packages/deck.gl-cog/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts index bd46d710..7212c320 100644 --- a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts +++ b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts @@ -65,7 +65,7 @@ export async function parseCOGTileMatrixSet( ]; // Starting from 1 to skip full res image - for (let imageIdx = 1; imageIdx < imageCount - 1; imageIdx++) { + for (let imageIdx = 1; imageIdx < imageCount; imageIdx++) { const image = await tiff.getImage(imageIdx); const tileMatrix = createOverviewTileMatrix({ id: String(imageCount - 1 - imageIdx), diff --git a/packages/deck.gl-cog/src/index.ts b/packages/deck.gl-cog/src/index.ts index eb1892cb..bed31e9a 100644 --- a/packages/deck.gl-cog/src/index.ts +++ b/packages/deck.gl-cog/src/index.ts @@ -4,3 +4,4 @@ export { } from "./geotiff-reprojection.js"; export { COGLayer } from "./cog-layer.js"; export type { COGLayerProps } from "./cog-layer.js"; +export { parseCOGTileMatrixSet } from "./cog-tile-matrix-set.js"; From 90378bc1b4fc3755517314dea4b08c4ff08132ef Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:04:03 -0500 Subject: [PATCH 08/12] Add small tms creation test --- packages/deck.gl-cog/tests/cog-tms.test.ts | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 packages/deck.gl-cog/tests/cog-tms.test.ts diff --git a/packages/deck.gl-cog/tests/cog-tms.test.ts b/packages/deck.gl-cog/tests/cog-tms.test.ts new file mode 100644 index 00000000..c26e45df --- /dev/null +++ b/packages/deck.gl-cog/tests/cog-tms.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from "vitest"; +import { parseCOGTileMatrixSet } from "../src"; +import { fromUrl } from "geotiff"; + +describe("create TileMatrixSet from COG", () => { + it("creates TMS", async () => { + const url = + "https://ds-wheels.s3.us-east-1.amazonaws.com/m_4007307_sw_18_060_20220803.tif"; + + const tiff = await fromUrl(url); + const tms = await parseCOGTileMatrixSet(tiff); + + const expectedTileMatrices = [ + { + id: "0", + scaleDenominator: 68684.95742667928, + cellSize: 19.231788079470196, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 1, + matrixHeight: 1, + geotransform: [ + 19.231788079470196, 0, 647118, 0, -19.231788079470196, 4533600, + ], + }, + { + id: "1", + scaleDenominator: 34285.71428571429, + cellSize: 9.6, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 2, + matrixHeight: 2, + geotransform: [9.6, 0, 647118, 0, -9.6, 4533600], + }, + { + id: "2", + scaleDenominator: 17142.857142857145, + cellSize: 4.8, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 3, + matrixHeight: 4, + geotransform: [4.8, 0, 647118, 0, -4.8, 4533600], + }, + { + id: "3", + scaleDenominator: 8571.428571428572, + cellSize: 2.4, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 5, + matrixHeight: 7, + geotransform: [2.4, 0, 647118, 0, -2.4, 4533600], + }, + { + id: "4", + scaleDenominator: 4285.714285714286, + cellSize: 1.2, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 10, + matrixHeight: 13, + geotransform: [1.2, 0, 647118, 0, -1.2, 4533600], + }, + { + id: "5", + scaleDenominator: 2142.857142857143, + cellSize: 0.6, + pointOfOrigin: [647118, 4533600], + tileWidth: 512, + tileHeight: 512, + matrixWidth: 19, + matrixHeight: 25, + geotransform: [0.6, 0, 647118, 0, -0.6, 4533600], + }, + ]; + + expect(tms.tileMatrices).toStrictEqual(expectedTileMatrices); + }); +}); From e0ebb6af40a3be34d0099c8d03466af7edf35537 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:36:01 -0500 Subject: [PATCH 09/12] Clean up and add correct metersPerUnit handling --- .../deck.gl-cog/src/cog-tile-matrix-set.ts | 78 ++++++++- packages/deck.gl-cog/src/ellipsoids.ts | 154 ++++++++++++++++++ 2 files changed, 224 insertions(+), 8 deletions(-) create mode 100644 packages/deck.gl-cog/src/ellipsoids.ts diff --git a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts index 7212c320..3db11747 100644 --- a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts +++ b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts @@ -7,7 +7,9 @@ import { extractGeotransform, getGeoTIFFProjection, } from "./geotiff-reprojection"; -import proj4 from "proj4"; +import proj4, { ProjectionDefinition } from "proj4"; +import Ellipsoid from "./ellipsoids.js"; +import type { PROJJSONDefinition } from "proj4/dist/lib/core"; // 0.28 mm per pixel // https://docs.ogc.org/is/17-083r4/17-083r4.html#toc15 @@ -24,17 +26,26 @@ export async function parseCOGTileMatrixSet( tiff: GeoTIFF, ): Promise { const fullResImage = await tiff.getImage(); + + if (!fullResImage.isTiled) { + throw new Error("COG TileMatrixSet requires a tiled GeoTIFF"); + } + const imageCount = await tiff.getImageCount(); const bbox = fullResImage.getBoundingBox(); const fullImageWidth = fullResImage.getWidth(); const fullImageHeight = fullResImage.getHeight(); const crs = await getGeoTIFFProjection(fullResImage); + if (crs === null) { throw new Error( "Could not determine coordinate reference system from GeoTIFF geo keys", ); } + + const parsedCrs = parseCrs(crs); + const projectToWgs84 = proj4(crs, "EPSG:4326").forward; const projectTo3857 = proj4(crs, "EPSG:3857").forward; @@ -44,6 +55,14 @@ export async function parseCOGTileMatrixSet( }; const transform = extractGeotransform(fullResImage); + + if (transform[1] !== 0 || transform[3] !== 0) { + // TileMatrixSet assumes orthogonal axes + throw new Error( + "COG TileMatrixSet with rotation/skewed geotransform is not supported", + ); + } + const cellSize = Math.abs(transform[0]); const tileWidth = fullResImage.getTileWidth(); @@ -53,7 +72,8 @@ export async function parseCOGTileMatrixSet( { // Set as highest resolution / finest level id: String(imageCount - 1), - scaleDenominator: (cellSize * metersPerUnit()) / SCREEN_PIXEL_SIZE, + scaleDenominator: + (cellSize * metersPerUnit(parsedCrs)) / SCREEN_PIXEL_SIZE, cellSize, pointOfOrigin: [transform[2], transform[5]], tileWidth: fullResImage.getTileWidth(), @@ -67,12 +87,18 @@ export async function parseCOGTileMatrixSet( // Starting from 1 to skip full res image for (let imageIdx = 1; imageIdx < imageCount; imageIdx++) { const image = await tiff.getImage(imageIdx); + + if (!image.isTiled) { + throw new Error("COG TileMatrixSet requires a tiled GeoTIFF"); + } + const tileMatrix = createOverviewTileMatrix({ id: String(imageCount - 1 - imageIdx), image, fullWidth: fullImageWidth, fullHeight: fullImageHeight, baseTransform: transform, + parsedCrs, }); tileMatrices.push(tileMatrix); } @@ -100,9 +126,25 @@ export async function parseCOGTileMatrixSet( * > (a is the Earth maximum radius of the ellipsoid). */ // https://github.com/developmentseed/morecantile/blob/7c95a11c491303700d6e33e9c1607f2719584dec/morecantile/utils.py#L67-L90 -function metersPerUnit(): number { - // For now, we assume our projection is in meters - return 1; +function metersPerUnit(parsedCrs: ProjectionDefinition): number { + switch (parsedCrs.units) { + case "metre": + case "meter": + case "meters": + return 1; + case "foot": + return 0.3048; + case "US survey foot": + return 1200 / 3937; + } + + if (parsedCrs.units === "degree") { + // 2 * π * ellipsoid semi-major-axis / 360 + const { a } = Ellipsoid[parsedCrs.ellps as keyof typeof Ellipsoid]; + return (2 * Math.PI * a) / 360; + } + + throw new Error(`Unsupported CRS units: ${parsedCrs.units}`); } /** @@ -112,19 +154,28 @@ function createOverviewTileMatrix({ id, image, fullWidth, + fullHeight, baseTransform, + parsedCrs, }: { id: string; image: GeoTIFFImage; fullWidth: number; fullHeight: number; baseTransform: [number, number, number, number, number, number]; + parsedCrs: ProjectionDefinition; }): TileMatrix { const width = image.getWidth(); const height = image.getHeight(); - const scale = fullWidth / width; - // assert scale is same for x and y? + const scaleX = fullWidth / width; + const scaleY = fullHeight / height; + + if (Math.abs(scaleX - scaleY) > 1e-6) { + throw new Error("Non-uniform overview scaling detected (X/Y differ)"); + } + + const scale = scaleX; const geotransform: [number, number, number, number, number, number] = [ baseTransform[0] * scale, @@ -141,7 +192,7 @@ function createOverviewTileMatrix({ return { id, - scaleDenominator: (cellSize * metersPerUnit()) / SCREEN_PIXEL_SIZE, + scaleDenominator: (cellSize * metersPerUnit(parsedCrs)) / SCREEN_PIXEL_SIZE, cellSize, pointOfOrigin: [geotransform[2], geotransform[5]], tileWidth, @@ -151,3 +202,14 @@ function createOverviewTileMatrix({ geotransform, }; } + +function parseCrs(crs: PROJJSONDefinition): ProjectionDefinition { + // If you pass proj4.defs a projjson, it doesn't parse it; it just returns the + // input. + // + // Instead, you need to assign it to an alias and then retrieve it. + + const key = "__deck.gl-cog-internal__"; + proj4.defs(key, crs); + return proj4.defs(key); +} diff --git a/packages/deck.gl-cog/src/ellipsoids.ts b/packages/deck.gl-cog/src/ellipsoids.ts new file mode 100644 index 00000000..218fa937 --- /dev/null +++ b/packages/deck.gl-cog/src/ellipsoids.ts @@ -0,0 +1,154 @@ +/** + * Vendored and edited from proj4.js. + * + * In the implementation of metersPerUnit while generated a COG TileMatrixSet, + * we need to know the size of the semi major axis in the case the CRS is in + * degrees. + * + * https://github.com/proj4js/proj4js/blob/e90e5fa6872a1ffc40edb161cbeb4bd5e3bd9db5/lib/constants/Ellipsoid.js + */ + +const ellipsoids = { + MERIT: { + a: 6378137, + }, + SGS85: { + a: 6378136, + }, + GRS80: { + a: 6378137, + }, + IAU76: { + a: 6378140, + }, + airy: { + a: 6377563.396, + b: 6356256.91, + }, + APL4: { + a: 6378137, + }, + NWL9D: { + a: 6378145, + }, + mod_airy: { + a: 6377340.189, + b: 6356034.446, + }, + andrae: { + a: 6377104.43, + }, + aust_SA: { + a: 6378160, + }, + GRS67: { + a: 6378160, + }, + bessel: { + a: 6377397.155, + }, + bess_nam: { + a: 6377483.865, + }, + clrk66: { + a: 6378206.4, + b: 6356583.8, + }, + clrk80: { + a: 6378249.145, + }, + clrk80ign: { + a: 6378249.2, + b: 6356515, + }, + clrk58: { + a: 6378293.645208759, + }, + CPM: { + a: 6375738.7, + }, + delmbr: { + a: 6376428, + }, + engelis: { + a: 6378136.05, + }, + evrst30: { + a: 6377276.345, + }, + evrst48: { + a: 6377304.063, + }, + evrst56: { + a: 6377301.243, + }, + evrst69: { + a: 6377295.664, + }, + evrstSS: { + a: 6377298.556, + }, + fschr60: { + a: 6378166, + }, + fschr60m: { + a: 6378155, + }, + fschr68: { + a: 6378150, + }, + helmert: { + a: 6378200, + }, + hough: { + a: 6378270, + }, + intl: { + a: 6378388, + }, + kaula: { + a: 6378163, + }, + lerch: { + a: 6378139, + }, + mprts: { + a: 6397300, + }, + new_intl: { + a: 6378157.5, + b: 6356772.2, + }, + plessis: { + a: 6376523, + }, + krass: { + a: 6378245, + }, + SEasia: { + a: 6378155, + b: 6356773.3205, + }, + walbeck: { + a: 6376896, + b: 6355834.8467, + }, + WGS60: { + a: 6378165, + }, + WGS66: { + a: 6378145, + }, + WGS7: { + a: 6378135, + }, + WGS84: { + a: 6378137, + }, + sphere: { + a: 6370997, + b: 6370997, + }, +}; + +export default ellipsoids; From 4186a7a35daf327cc91cc79f2dbc29826651f518 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:39:19 -0500 Subject: [PATCH 10/12] remove not-ready code --- packages/deck.gl-raster/src/index.ts | 2 +- .../src/raster-tileset/index.ts | 0 .../raster-tileset/raster-tile-traversal.ts | 530 ------------------ .../src/raster-tileset/raster-tileset-2d.ts | 344 ------------ .../src/raster-tileset/types.ts | 217 ------- 5 files changed, 1 insertion(+), 1092 deletions(-) delete mode 100644 packages/deck.gl-raster/src/raster-tileset/index.ts delete mode 100644 packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts delete mode 100644 packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts delete mode 100644 packages/deck.gl-raster/src/raster-tileset/types.ts diff --git a/packages/deck.gl-raster/src/index.ts b/packages/deck.gl-raster/src/index.ts index 47f84663..88f38c1f 100644 --- a/packages/deck.gl-raster/src/index.ts +++ b/packages/deck.gl-raster/src/index.ts @@ -1,3 +1,3 @@ export { RasterLayer } from "./raster-layer.js"; export type { RasterLayerProps } from "./raster-layer.js"; -export type { TileMatrixSet, TileMatrix } from "./raster-tileset/types.js"; +// export type { TileMatrixSet, TileMatrix } from "./raster-tileset-bak/types.js"; diff --git a/packages/deck.gl-raster/src/raster-tileset/index.ts b/packages/deck.gl-raster/src/raster-tileset/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts deleted file mode 100644 index bb7a3ebb..00000000 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts +++ /dev/null @@ -1,530 +0,0 @@ -/** - * This file implements tile traversal for generic 2D tilesets defined by COG - * tile layouts. - * - * The main algorithm works as follows: - * 1. Start at the root tile(s) (z=0, covers the entire image, but not - * necessarily the whole world) - * 2. Test if each tile is visible using viewport frustum culling - * 3. For visible tiles, compute distance-based LOD (Level of Detail) - * 4. If LOD is insufficient, recursively subdivide into 4 child tiles - * 5. Select tiles at appropriate zoom levels based on distance from camera - * - * The result is a set of tiles at varying zoom levels that efficiently - * cover the visible area with appropriate detail. - */ - -import { - _GlobeViewport, - assert, - Viewport, - WebMercatorViewport, -} from "@deck.gl/core"; -import { - CullingVolume, - Plane, - makeOrientedBoundingBoxFromPoints, -} from "@math.gl/culling"; - -import type { TileMatrixSet, TileMatrix, TileIndex, ZRange } from "./types.js"; - -/** - * The size of the entire world in deck.gl's common coordinate space. - * - * The world always spans [0, 512] in both X and Y in Web Mercator common space. - * - * The origin (0,0) is at the top-left corner, and (512,512) is at the - * bottom-right. - */ -const WORLD_SIZE = 512; - -// Reference points used to sample tile boundaries for bounding volume -// calculation. -// -// In upstream deck.gl code, such reference points are only used in non-Web -// Mercator projections because the OSM tiling scheme is designed for Web -// Mercator and the OSM tile extents are already in Web Mercator projection. So -// using Axis-Aligned bounding boxes based on tile extents is sufficient for -// frustum culling in Web Mercator viewports. -// -// In upstream code these reference points are used for Globe View where the OSM -// tile indices _projected into longitude-latitude bounds in Globe View space_ -// are no longer axis-aligned, and oriented bounding boxes must be used instead. -// -// In the context of generic tiling grids which are often not in Web Mercator -// projection, we must use the reference points approach because the grid tiles -// will never be exact axis aligned boxes in Web Mercator space. - -// For most tiles: sample 4 corners and center (5 points total) -const REF_POINTS_5: [number, number][] = [ - [0.5, 0.5], // center - [0, 0], // top-left - [0, 1], // bottom-left - [1, 0], // top-right - [1, 1], // bottom-right -]; - -// For higher detail: add 4 edge midpoints (9 points total) -const REF_POINTS_9 = REF_POINTS_5.concat([ - [0, 0.5], // left edge - [0.5, 0], // top edge - [1, 0.5], // right edge - [0.5, 1], // bottom edge -]); - -/** - * Raster Tile Node - similar to OSMNode but for a generic raster tileset's - * tile structure. - * - * Represents a single tile in the COG internal tiling pyramid. - * - * COG tile nodes use the following coordinate system: - * - * - x: tile column (0 to TileMatrix.tilesX, left to right) - * - y: tile row (0 to TileMatrix.tilesY, top to bottom) - * - z: overview level. This uses TileMatrixSet ordering where: 0 = coarsest, higher = finer - */ -export class RasterTileNode { - /** Index across a row */ - x: number; - /** Index down a column */ - y: number; - /** TileMatrixSet-style zoom index (higher = finer detail) */ - z: number; - - private metadata: TileMatrixSet; - - /** - * Flag indicating whether any descendant of this tile is visible. - * - * Used to prevent loading parent tiles when children are visible (avoids - * overdraw). - */ - private childVisible?: boolean; - - /** - * Flag indicating this tile should be rendered - * - * Set to `true` when this is the appropriate LOD for its distance from camera. - */ - private selected?: boolean; - - /** A cache of the children of this node. */ - private _children?: RasterTileNode[]; - - constructor(x: number, y: number, z: number, metadata: TileMatrixSet) { - this.x = x; - this.y = y; - this.z = z; - this.metadata = metadata; - } - - /** Get overview info for this tile's z level */ - get overview(): TileMatrix { - return this.metadata.tileMatrices[this.z]!; - } - - /** Get the children of this node. */ - get children(): RasterTileNode[] { - if (!this._children) { - const maxZ = this.metadata.tileMatrices.length - 1; - if (this.z >= maxZ) { - // Already at finest resolution, no children - return []; - } - - // In TileMatrixSet ordering: refine to z + 1 (finer detail) - const childZ = this.z + 1; - const parentOverview = this.overview; - const childOverview = this.metadata.tileMatrices[childZ]!; - - // Calculate scale factor between levels - const scaleFactor = parentOverview.cellSize / childOverview.cellSize; - - // Generate child tiles - this._children = []; - for (let dy = 0; dy < scaleFactor; dy++) { - for (let dx = 0; dx < scaleFactor; dx++) { - const childX = this.x * scaleFactor + dx; - const childY = this.y * scaleFactor + dy; - - // Only create child if it's within bounds - // Some tiles on the edges might not need to be created at higher - // resolutions (higher map zoom level) - if ( - childX < childOverview.tileWidth && - childY < childOverview.tileHeight - ) { - this._children.push( - new RasterTileNode(childX, childY, childZ, this.metadata), - ); - } - } - } - } - return this._children; - } - - /** - * Update tile visibility using frustum culling - * This follows the pattern from OSMNode - */ - update(params: { - viewport: Viewport; - project: ((xyz: number[]) => number[]) | null; - cullingVolume: CullingVolume; - elevationBounds: ZRange; - /** Minimum (coarsest) COG overview level */ - minZ: number; - /** Maximum (finest) COG overview level */ - maxZ?: number; - }): boolean { - const { - viewport, - cullingVolume, - elevationBounds, - minZ, - maxZ = this.metadata.tileMatrices.length - 1, - project, - } = params; - - // Get bounding volume for this tile - const boundingVolume = this.getBoundingVolume(elevationBounds, project); - - // Note: this is a part of the upstream code because they have _generic_ - // tiling systems, where the client doesn't know whether a given xyz tile - // actually exists. So the idea of `bounds` is to avoid even trying to fetch - // tiles that the user doesn't care about (think oceans) - // - // But in our case, we have known bounds from the COG metadata. So the tiles - // are explicitly constructed to match only tiles that exist. - - // Check if tile is within user-specified bounds - // if (bounds && !this.insideBounds(bounds)) { - // return false; - // } - - console.log("=== FRUSTUM CULLING DEBUG ==="); - console.log(`Tile: ${this.x}, ${this.y}, ${this.z}`); - console.log("Bounding volume center:", boundingVolume.center); - console.log("Bounding volume halfSize:", boundingVolume.halfSize); - console.log("Viewport cameraPosition:", viewport.cameraPosition); - console.log( - "Viewport pitch:", - viewport instanceof WebMercatorViewport ? viewport.pitch : "N/A", - ); - - for (let i = 0; i < cullingVolume.planes.length; i++) { - const plane = cullingVolume.planes[i]!; - const result = boundingVolume.intersectPlane(plane); - const planeNames = ["left", "right", "bottom", "top", "near", "far"]; - - // Calculate signed distance from OBB center to plane - const centerDist = - plane.normal.x * boundingVolume.center.x + - plane.normal.y * boundingVolume.center.y + - plane.normal.z * boundingVolume.center.z + - plane.distance; - - console.log( - `Plane ${i} (${planeNames[i]}): normal=[${plane.normal.x.toFixed(3)}, ${plane.normal.y.toFixed(3)}, ${plane.normal.z.toFixed(3)}], ` + - `distance=${plane.distance.toFixed(3)}, centerDist=${centerDist.toFixed(3)}, result=${result} (${result === 1 ? "INSIDE" : result === 0 ? "INTERSECT" : "OUTSIDE"})`, - ); - } - console.log("=== END FRUSTUM DEBUG ==="); - - // Frustum culling - // Test if tile's bounding volume intersects the camera frustum - // Returns: <0 if outside, 0 if intersecting, >0 if fully inside - const isInside = cullingVolume.computeVisibility(boundingVolume); - console.log( - `Tile ${this.x},${this.y},${this.z} frustum check: ${isInside} (${isInside < 0 ? "CULLED" : "VISIBLE"})`, - ); - if (isInside < 0) { - return false; - } - - // LOD (Level of Detail) selection - // Only select this tile if no child is visible (prevents overlapping tiles) - if (!this.childVisible) { - let { z } = this; - - if (z < maxZ && z >= minZ) { - // Compute distance-based LOD adjustment - // Tiles farther from camera can use lower zoom levels (larger tiles) - // Distance is normalized by viewport height to be resolution-independent - const distance = - (boundingVolume.distanceTo(viewport.cameraPosition) * - viewport.scale) / - viewport.height; - // Increase effective zoom level based on log2(distance) - // e.g., if distance=4, accept tiles 2 levels lower than maxZ - z += Math.floor(Math.log2(distance)); - } - - if (z >= maxZ) { - // This tile's LOD is sufficient for its distance - select it for rendering - this.selected = true; - return true; - } - } - - // LOD is not enough, recursively test child tiles - this.selected = false; - this.childVisible = true; - - for (const child of this.children) { - child.update(params); - } - - // // NOTE: this deviates from upstream; we could move to the upstream code if - // // we pass in maxZ correctly I think - // if (children.length === 0) { - // // No children available (at finest resolution), select this tile - // this.selected = true; - // return true; - // } - - // for (const child of children) { - // child.update(params); - // } - return true; - } - - /** - * Collect all tiles marked as selected in the tree. - * Recursively traverses the entire tree and gathers tiles where selected=true. - * - * @param result - Accumulator array for selected tiles - * @returns Array of selected OSMNode tiles - */ - getSelected(result: RasterTileNode[] = []): RasterTileNode[] { - if (this.selected) { - result.push(this); - } - if (this._children) { - for (const node of this._children) { - node.getSelected(result); - } - } - return result; - } - - /** - * Calculate the 3D bounding volume for this tile in deck.gl's common - * coordinate space for frustum culling. - * - */ - getBoundingVolume( - zRange: ZRange, - project: ((xyz: number[]) => number[]) | null, - ) { - const overview = this.overview; - const { tileMatrices } = this.metadata; - const { tileWidth, tileHeight } = tileMatrices[tileMatrices.length - 1]!; - - // Use geotransform to calculate tile bounds - // geotransform: [a, b, c, d, e, f] where: - // x_geo = a * col + b * row + c - // y_geo = d * col + e * row + f - const [a, b, c, d, e, f] = overview.geotransform; - - // Calculate pixel coordinates for this tile's extent - const pixelMinCol = this.x * tileWidth; - const pixelMinRow = this.y * tileHeight; - const pixelMaxCol = (this.x + 1) * tileWidth; - const pixelMaxRow = (this.y + 1) * tileHeight; - - // Sample reference points across the tile surface - const refPoints = REF_POINTS_9; - - console.log("refPoints", refPoints); - - /** Reference points positions in image CRS */ - const refPointPositionsImage: [number, number][] = []; - - for (const [pX, pY] of refPoints) { - // pX, pY are in [0, 1] range - // Interpolate pixel coordinates within the tile - const col = pixelMinCol + pX * (pixelMaxCol - pixelMinCol); - const row = pixelMinRow + pY * (pixelMaxRow - pixelMinRow); - - // Convert pixel coordinates to geographic coordinates using geotransform - const geoX = a * col + b * row + c; - const geoY = d * col + e * row + f; - - refPointPositionsImage.push([geoX, geoY]); - } - - console.log("refPointPositionsImage (image CRS):", refPointPositionsImage); - console.log("Geotransform [a,b,c,d,e,f]:", [a, b, c, d, e, f]); - - if (project) { - assert( - false, - "TODO: implement bounding volume implementation in Globe view", - ); - // Reproject positions to wgs84 instead, then pass them into `project` - // return makeOrientedBoundingBoxFromPoints(refPointPositions); - } - - /** Reference points positions in EPSG 3857 */ - const refPointPositionsProjected: [number, number][] = []; - - for (const [pX, pY] of refPointPositionsImage) { - // Reproject to Web Mercator (EPSG 3857) - const projected = this.metadata.projectTo3857([pX, pY]); - refPointPositionsProjected.push(projected); - - // Also log WGS84 for comparison - const wgs84 = this.metadata.projectToWgs84([pX, pY]); - console.log( - `Image [${pX.toFixed(2)}, ${pY.toFixed(2)}] -> WGS84 [${wgs84[0].toFixed(6)}, ${wgs84[1].toFixed(6)}] -> WebMerc [${projected[0].toFixed(2)}, ${projected[1].toFixed(2)}]`, - ); - } - - console.log( - "refPointPositionsProjected (EPSG:3857):", - refPointPositionsProjected, - ); - - // Convert from Web Mercator meters to deck.gl's common space (world units) - // Web Mercator range: [-20037508.34, 20037508.34] meters - // deck.gl world space: [0, 512] - const WEB_MERCATOR_MAX = 20037508.342789244; // Half Earth circumference - - /** Reference points positions in deck.gl world space */ - const refPointPositionsWorld: [number, number, number][] = []; - - for (const [mercX, mercY] of refPointPositionsProjected) { - // X: offset from [-20M, 20M] to [0, 40M], then normalize to [0, 512] - const worldX = - ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; - - // Y: same transformation WITHOUT flip - // Testing hypothesis: Y-flip might be incorrect since geotransform already handles orientation - const worldY = - ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; - - console.log( - `WebMerc [${mercX.toFixed(2)}, ${mercY.toFixed(2)}] -> World [${worldX.toFixed(4)}, ${worldY.toFixed(4)}]`, - ); - - // Add z-range minimum - refPointPositionsWorld.push([worldX, worldY, zRange[0]]); - } - - // Add top z-range if elevation varies - if (zRange[0] !== zRange[1]) { - for (const [mercX, mercY] of refPointPositionsProjected) { - const worldX = - ((mercX + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; - const worldY = - WORLD_SIZE - - ((mercY + WEB_MERCATOR_MAX) / (2 * WEB_MERCATOR_MAX)) * WORLD_SIZE; - - refPointPositionsWorld.push([worldX, worldY, zRange[1]]); - } - } - - console.log("refPointPositionsWorld", refPointPositionsWorld); - console.log("zRange used:", zRange); - - const obb = makeOrientedBoundingBoxFromPoints(refPointPositionsWorld); - console.log("Created OBB center:", obb.center); - console.log("Created OBB halfAxes:", obb.halfAxes); - - return obb; - } - - /** - * Convert COG coordinates to lng/lat - * This is a placeholder - needs proper projection library (proj4js) - */ - private cogCoordsToLngLat([x, y]: [number, number]): number[] { - const [lng, lat] = this.metadata.projectToWgs84([x, y]); - return [lng, lat, 0]; - } -} - -/** - * Get tile indices visible in viewport - * Uses frustum culling similar to OSM implementation - * - * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer - */ -export function getTileIndices( - metadata: TileMatrixSet, - opts: { - viewport: Viewport; - maxZ: number; - zRange: ZRange | null; - }, -): TileIndex[] { - const { viewport, maxZ, zRange } = opts; - - // console.log("=== getTileIndices called ==="); - // console.log("Viewport:", viewport); - // console.log("maxZ:", maxZ); - // console.log("COG metadata tileMatrices count:", metadata.tileMatrices.length); - // console.log("COG bbox:", metadata.bbox); - - const project: ((xyz: number[]) => number[]) | null = - viewport instanceof _GlobeViewport && viewport.resolution - ? viewport.projectPosition - : null; - - // Get the culling volume of the current camera - const planes: Plane[] = Object.values(viewport.getFrustumPlanes()).map( - ({ normal, distance }) => new Plane(normal.clone().negate(), distance), - ); - const cullingVolume = new CullingVolume(planes); - - // Project zRange from meters to common space - const unitsPerMeter = viewport.distanceScales.unitsPerMeter[2]!; - const elevationMin = (zRange && zRange[0] * unitsPerMeter) || 0; - const elevationMax = (zRange && zRange[1] * unitsPerMeter) || 0; - - // Optimization: For low-pitch views, only consider tiles at maxZ level - // At low pitch (top-down view), all tiles are roughly the same distance, - // so we don't need the LOD pyramid - just use the finest level - const minZ = - viewport instanceof WebMercatorViewport && viewport.pitch <= 60 ? maxZ : 0; - - // Start from coarsest overview - const coarsestOverview = metadata.tileMatrices[0]!; - - // Create root tiles at coarsest level - // In contrary to OSM tiling, we might have more than one tile at the - // coarsest level (z=0) - const roots: RasterTileNode[] = []; - for (let y = 0; y < coarsestOverview.tileHeight; y++) { - for (let x = 0; x < coarsestOverview.tileWidth; x++) { - roots.push(new RasterTileNode(x, y, 0, metadata)); - } - } - - // Traverse and update visibility - const traversalParams = { - viewport, - project, - cullingVolume, - elevationBounds: [elevationMin, elevationMax] as ZRange, - minZ, - maxZ, - }; - console.log("Traversal params:", traversalParams); - - for (const root of roots) { - root.update(traversalParams); - } - console.log("roots", roots); - - // Collect selected tiles - const selectedNodes: RasterTileNode[] = []; - for (const root of roots) { - root.getSelected(selectedNodes); - } - - return selectedNodes; -} diff --git a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts b/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts deleted file mode 100644 index e3e8ccfe..00000000 --- a/packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts +++ /dev/null @@ -1,344 +0,0 @@ -/** - * COGTileset2D - Improved Implementation with Frustum Culling - * - * This version properly implements frustum culling and bounding volume calculations - * following the pattern from deck.gl's OSM tile indexing. - */ - -import { Viewport, WebMercatorViewport } from "@deck.gl/core"; -import { _Tileset2D as Tileset2D } from "@deck.gl/geo-layers"; -import type { Tileset2DProps } from "@deck.gl/geo-layers/dist/tileset-2d"; -import type { ZRange } from "@deck.gl/geo-layers/dist/tileset-2d/types"; -import { Matrix4 } from "@math.gl/core"; -import { GeoTIFF, GeoTIFFImage } from "geotiff"; -import proj4 from "proj4"; - -import { getTileIndices } from "./raster-tile-traversal"; -import type { COGMetadata, COGTileIndex, COGOverview, Bounds } from "./types"; - -const OGC_84 = { - $schema: "https://proj.org/schemas/v0.7/projjson.schema.json", - type: "GeographicCRS", - name: "WGS 84 (CRS84)", - datum_ensemble: { - name: "World Geodetic System 1984 ensemble", - members: [ - { - name: "World Geodetic System 1984 (Transit)", - id: { authority: "EPSG", code: 1166 }, - }, - { - name: "World Geodetic System 1984 (G730)", - id: { authority: "EPSG", code: 1152 }, - }, - { - name: "World Geodetic System 1984 (G873)", - id: { authority: "EPSG", code: 1153 }, - }, - { - name: "World Geodetic System 1984 (G1150)", - id: { authority: "EPSG", code: 1154 }, - }, - { - name: "World Geodetic System 1984 (G1674)", - id: { authority: "EPSG", code: 1155 }, - }, - { - name: "World Geodetic System 1984 (G1762)", - id: { authority: "EPSG", code: 1156 }, - }, - { - name: "World Geodetic System 1984 (G2139)", - id: { authority: "EPSG", code: 1309 }, - }, - ], - ellipsoid: { - name: "WGS 84", - semi_major_axis: 6378137, - inverse_flattening: 298.257223563, - }, - accuracy: "2.0", - id: { authority: "EPSG", code: 6326 }, - }, - coordinate_system: { - subtype: "ellipsoidal", - axis: [ - { - name: "Geodetic longitude", - abbreviation: "Lon", - direction: "east", - unit: "degree", - }, - { - name: "Geodetic latitude", - abbreviation: "Lat", - direction: "north", - unit: "degree", - }, - ], - }, - scope: "Not known.", - area: "World.", - bbox: { - south_latitude: -90, - west_longitude: -180, - north_latitude: 90, - east_longitude: 180, - }, - id: { authority: "OGC", code: "CRS84" }, -}; - -/** - * Extract COG metadata - */ -export async function extractCOGMetadata(tiff: GeoTIFF): Promise { - const image = await tiff.getImage(); - - const width = image.getWidth(); - const height = image.getHeight(); - const tileWidth = image.getTileWidth(); - const tileHeight = image.getTileHeight(); - - const tilesX = Math.ceil(width / tileWidth); - const tilesY = Math.ceil(height / tileHeight); - - const bbox = image.getBoundingBox(); - const geoKeys = image.getGeoKeys(); - const projectionCode: number | null = - geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey || null; - const projection = projectionCode ? `EPSG:${projectionCode}` : null; - - // Extract geotransform from full-resolution image - // Only the top-level IFD has geo keys, so we'll derive overviews from this - const baseGeotransform = extractGeotransform(image); - - // Overviews **in COG order**, from finest to coarsest (we'll reverse the - // array later) - const overviews: COGOverview[] = []; - const imageCount = await tiff.getImageCount(); - - // Full resolution image (GeoTIFF index 0) - overviews.push({ - geoTiffIndex: 0, - width, - height, - tilesX, - tilesY, - scaleFactor: 1, - geotransform: baseGeotransform, - // TODO: combine these two properties into one - level: imageCount - 1, // Coarsest level number - z: imageCount - 1, - }); - - for (let i = 1; i < imageCount; i++) { - const overview = await tiff.getImage(i); - const overviewWidth = overview.getWidth(); - const overviewHeight = overview.getHeight(); - const overviewTileWidth = overview.getTileWidth(); - const overviewTileHeight = overview.getTileHeight(); - - const scaleFactor = Math.round(width / overviewWidth); - - // Derive geotransform for this overview by scaling pixel size - // [a, b, c, d, e, f] where a and e are pixel dimensions - const overviewGeotransform: [ - number, - number, - number, - number, - number, - number, - ] = [ - baseGeotransform[0] * scaleFactor, // a: scaled pixel width - baseGeotransform[1] * scaleFactor, // b: scaled row rotation - baseGeotransform[2], // c: same x origin - baseGeotransform[3] * scaleFactor, // d: scaled column rotation - baseGeotransform[4] * scaleFactor, // e: scaled pixel height (typically negative) - baseGeotransform[5], // f: same y origin - ]; - - overviews.push({ - geoTiffIndex: i, - width: overviewWidth, - height: overviewHeight, - tilesX: Math.ceil(overviewWidth / overviewTileWidth), - tilesY: Math.ceil(overviewHeight / overviewTileHeight), - scaleFactor, - geotransform: overviewGeotransform, - // TODO: combine these two properties into one - level: imageCount - 1 - i, - z: imageCount - 1 - i, - }); - } - - // Reverse to TileMatrixSet order: coarsest (0) → finest (n) - overviews.reverse(); - - const sourceProjection = await getProjjson(projectionCode); - const projectToWgs84 = proj4(sourceProjection, OGC_84); - const projectTo3857 = proj4(sourceProjection, "EPSG:3857"); - - return { - width, - height, - tileWidth, - tileHeight, - tilesX, - tilesY, - bbox: [bbox[0], bbox[1], bbox[2], bbox[3]], - projection, - projectToWgs84, - projectTo3857, - overviews, - image: tiff, - }; -} - -async function getProjjson(projectionCode: number | null) { - const url = `https://epsg.io/${projectionCode}.json`; - const response = await fetch(url); - if (!response.ok) { - throw new Error(`Failed to fetch projection data from ${url}`); - } - const data = await response.json(); - return data; -} - -const viewport = new WebMercatorViewport({ - height: 500, - width: 845, - latitude: 40.88775942857086, - longitude: -73.20197979318772, - zoom: 11.294596276534985, -}); - -/** - * COGTileset2D with proper frustum culling - */ -export class COGTileset2D extends Tileset2D { - private cogMetadata: COGMetadata; - - constructor(cogMetadata: COGMetadata, opts: Tileset2DProps) { - super(opts); - this.cogMetadata = cogMetadata; - } - - /** - * Get tile indices visible in viewport - * Uses frustum culling similar to OSM implementation - * - * Overviews follow TileMatrixSet ordering: index 0 = coarsest, higher = finer - */ - getTileIndices(opts: { - viewport: Viewport; - maxZoom?: number; - minZoom?: number; - zRange: ZRange | null; - modelMatrix?: Matrix4; - modelMatrixInverse?: Matrix4; - }): COGTileIndex[] { - console.log("Called getTileIndices", opts); - const tileIndices = getTileIndices(this.cogMetadata, opts); - console.log("Visible tile indices:", tileIndices); - - // return [ - // { x: 0, y: 0, z: 0 }, - // { x: 0, y: 0, z: 1 }, - // { x: 1, y: 1, z: 2 }, - // { x: 1, y: 2, z: 3 }, - // { x: 2, y: 1, z: 3 }, - // { x: 2, y: 2, z: 3 }, - // { x: 3, y: 1, z: 3 }, - // ]; // Temporary override for testing - return tileIndices; - } - - getTileId(index: COGTileIndex): string { - return `${index.x}-${index.y}-${index.z}`; - } - - getParentIndex(index: COGTileIndex): COGTileIndex { - if (index.z === 0) { - // Already at coarsest level - return index; - } - - const currentOverview = this.cogMetadata.overviews[index.z]; - const parentOverview = this.cogMetadata.overviews[index.z - 1]; - - const scaleFactor = - currentOverview.scaleFactor / parentOverview.scaleFactor; - - return { - x: Math.floor(index.x / scaleFactor), - y: Math.floor(index.y / scaleFactor), - z: index.z - 1, - }; - } - - getTileZoom(index: COGTileIndex): number { - return index.z; - } - - getTileMetadata(index: COGTileIndex): Record { - const { x, y, z } = index; - const { overviews, tileWidth, tileHeight } = this.cogMetadata; - const overview = overviews[z]; - - // Use geotransform to calculate tile bounds - // geotransform: [a, b, c, d, e, f] where: - // x_geo = a * col + b * row + c - // y_geo = d * col + e * row + f - const [a, b, c, d, e, f] = overview.geotransform; - - // Calculate pixel coordinates for this tile's extent - const pixelMinCol = x * tileWidth; - const pixelMinRow = y * tileHeight; - const pixelMaxCol = (x + 1) * tileWidth; - const pixelMaxRow = (y + 1) * tileHeight; - - // Calculate the four corners of the tile in geographic coordinates - const topLeft = [ - a * pixelMinCol + b * pixelMinRow + c, - d * pixelMinCol + e * pixelMinRow + f, - ]; - const topRight = [ - a * pixelMaxCol + b * pixelMinRow + c, - d * pixelMaxCol + e * pixelMinRow + f, - ]; - const bottomLeft = [ - a * pixelMinCol + b * pixelMaxRow + c, - d * pixelMinCol + e * pixelMaxRow + f, - ]; - const bottomRight = [ - a * pixelMaxCol + b * pixelMaxRow + c, - d * pixelMaxCol + e * pixelMaxRow + f, - ]; - - // Return the projected bounds as four corners - // This preserves rotation/skew information - const projectedBounds = { - topLeft, - topRight, - bottomLeft, - bottomRight, - }; - - // Also compute axis-aligned bounding box for compatibility - const bounds: Bounds = [ - Math.min(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), - Math.min(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), - Math.max(topLeft[0], topRight[0], bottomLeft[0], bottomRight[0]), - Math.max(topLeft[1], topRight[1], bottomLeft[1], bottomRight[1]), - ]; - - return { - bounds, - projectedBounds, - tileWidth, - tileHeight, - overview, - }; - } -} diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts deleted file mode 100644 index d0efe214..00000000 --- a/packages/deck.gl-raster/src/raster-tileset/types.ts +++ /dev/null @@ -1,217 +0,0 @@ -export type ZRange = [minZ: number, maxZ: number]; - -export type Bounds = [minX: number, minY: number, maxX: number, maxY: number]; - -export type GeoBoundingBox = { - west: number; - north: number; - east: number; - south: number; -}; -export type NonGeoBoundingBox = { - left: number; - top: number; - right: number; - bottom: number; -}; - -export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; - -export type TileLoadProps = { - index: TileIndex; - id: string; - bbox: TileBoundingBox; - url?: string | null; - signal?: AbortSignal; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - userData?: Record; - zoom?: number; -}; - -export type Point = [number, number]; - -type CRS = any; - -/** - * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere - */ -export interface TileMatrixSetBoundingBox { - lowerLeft: Point; - upperRight: Point; - crs?: CRS; -} - -/** - * Represents a single resolution level in a raster tileset. - * - * COGs contain multiple resolution levels (overviews) for efficient - * visualization at different zoom levels. - * - * IMPORTANT: Overviews are ordered according to TileMatrixSet specification: - * - Index 0: Coarsest resolution (most zoomed out) - * - Index N: Finest resolution (most zoomed in) - * - * This matches the natural ordering where z increases with detail. - */ -export type TileMatrix = { - id: string; - - /** - * Scale denominator of this tile matrix. - * - * Defined as cellSize (meters per pixel) * meters per unit / 0.00028 - */ - scaleDenominator: number; - - /** - * Cell size of this tile matrix. - * - * This is the pixel size in meters. - */ - cellSize: number; - - // /** - // * Overview index in the TileMatrixSet ordering. - // * - Index 0: Coarsest resolution (most zoomed out) - // * - Higher indices: Progressively finer resolution - // * - // * This is the index in the COGMetadata.overviews array and represents - // * the natural ordering from coarse to fine. - // * - // * Note: This is different from GeoTIFF's internal level numbering where - // * level 0 is the full resolution image. - // * - // * @example - // * // For a COG with 4 resolutions: - // * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) - // * index: 1 // Medium: 2500x2000 pixels (4x downsampled) - // * index: 2 // Fine: 5000x4000 pixels (2x downsampled) - // * index: 3 // Finest: 10000x8000 pixels (full resolution) - // */ - // z: number; - - pointOfOrigin: Point; - - /** - * Width of each tile of this tile matrix in pixels. - */ - tileWidth: number; - /** - * Height of each tile of this tile matrix in pixels. - */ - tileHeight: number; - - /** - * Number of tiles in the X (horizontal) direction at this overview level. - * - * Calculated as: Math.ceil(width / tileWidth) - * - * @example - * // If tileWidth = 512: - * tilesX: 3 // z=0: ceil(1250 / 512) - * tilesX: 5 // z=1: ceil(2500 / 512) - * tilesX: 10 // z=2: ceil(5000 / 512) - * tilesX: 20 // z=3: ceil(10000 / 512) - */ - matrixWidth: number; - - /** - * Number of tiles in the Y (vertical) direction at this overview level. - * - * Calculated as: Math.ceil(height / tileHeight) - * - * @example - * // If tileHeight = 512: - * tilesY: 2 // z=0: ceil(1000 / 512) - * tilesY: 4 // z=1: ceil(2000 / 512) - * tilesY: 8 // z=2: ceil(4000 / 512) - * tilesY: 16 // z=3: ceil(8000 / 512) - */ - matrixHeight: number; - - // /** - // * Downsampling scale factor relative to full resolution (finest level). - // * - // * Indicates how much this overview is downsampled compared to the finest resolution. - // * - Scale factor of 1: Full resolution (finest level) - // * - Scale factor of 2: Half resolution - // * - Scale factor of 4: Quarter resolution - // * - Scale factor of 8: Eighth resolution (coarsest in this example) - // * - // * Common pattern: Each overview is 2x downsampled from the next finer level, - // * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) - // * - // * @example - // * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) - // * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) - // * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) - // * scaleFactor: 1 // z=3: 10000x8000 (full resolution) - // */ - // scaleFactor: number; - - /** - * Affine geotransform for this overview level. - * - * Uses Python `affine` package ordering (NOT GDAL ordering): - * [a, b, c, d, e, f] where: - * - x_geo = a * col + b * row + c - * - y_geo = d * col + e * row + f - * - * Parameters: - * - a: pixel width (x resolution) - * - b: row rotation (typically 0) - * - c: x-coordinate of upper-left corner of the upper-left pixel - * - d: column rotation (typically 0) - * - e: pixel height (y resolution, typically negative) - * - f: y-coordinate of upper-left corner of the upper-left pixel - * - * @example - * // For a UTM image with 30m pixels: - * [30, 0, 440720, 0, -30, 3751320] - * // x_geo = 30 * col + 440720 - * // y_geo = -30 * row + 3751320 - */ - geotransform: [number, number, number, number, number, number]; -}; - -/** - * COG Metadata extracted from GeoTIFF - */ -export type TileMatrixSet = { - /** - * Title of this tile matrix set, normally used for display to a human - */ - title?: string; - /** - * Brief narrative description of this tile matrix set, normally available for display to a human - */ - description?: string; - crs: CRS; - - boundingBox?: TileMatrixSetBoundingBox; - /** - * Describes scale levels and its tile matrices - */ - tileMatrices: TileMatrix[]; - - projectToWgs84: (point: [number, number]) => [number, number]; - projectTo3857: (point: [number, number]) => [number, number]; -}; - -/** - * Raster Tile Index - * - * In TileMatrixSet ordering: `level === z`. - * - * So level `z` is the coarsest resolution (0) and the highest `z` is the finest - * resolution. - */ -export type TileIndex = { - x: number; - y: number; - - /** - * TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) - */ - z: number; -}; From a8bb4b9045c7a9e3033979cb7d67f1a0e04ef323 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:42:37 -0500 Subject: [PATCH 11/12] restore type definitions --- packages/deck.gl-raster/src/index.ts | 2 +- .../src/raster-tileset/types.ts | 217 ++++++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 packages/deck.gl-raster/src/raster-tileset/types.ts diff --git a/packages/deck.gl-raster/src/index.ts b/packages/deck.gl-raster/src/index.ts index 88f38c1f..47f84663 100644 --- a/packages/deck.gl-raster/src/index.ts +++ b/packages/deck.gl-raster/src/index.ts @@ -1,3 +1,3 @@ export { RasterLayer } from "./raster-layer.js"; export type { RasterLayerProps } from "./raster-layer.js"; -// export type { TileMatrixSet, TileMatrix } from "./raster-tileset-bak/types.js"; +export type { TileMatrixSet, TileMatrix } from "./raster-tileset/types.js"; diff --git a/packages/deck.gl-raster/src/raster-tileset/types.ts b/packages/deck.gl-raster/src/raster-tileset/types.ts new file mode 100644 index 00000000..d0efe214 --- /dev/null +++ b/packages/deck.gl-raster/src/raster-tileset/types.ts @@ -0,0 +1,217 @@ +export type ZRange = [minZ: number, maxZ: number]; + +export type Bounds = [minX: number, minY: number, maxX: number, maxY: number]; + +export type GeoBoundingBox = { + west: number; + north: number; + east: number; + south: number; +}; +export type NonGeoBoundingBox = { + left: number; + top: number; + right: number; + bottom: number; +}; + +export type TileBoundingBox = NonGeoBoundingBox | GeoBoundingBox; + +export type TileLoadProps = { + index: TileIndex; + id: string; + bbox: TileBoundingBox; + url?: string | null; + signal?: AbortSignal; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + userData?: Record; + zoom?: number; +}; + +export type Point = [number, number]; + +type CRS = any; + +/** + * Minimum bounding rectangle surrounding a 2D resource in the CRS indicated elsewhere + */ +export interface TileMatrixSetBoundingBox { + lowerLeft: Point; + upperRight: Point; + crs?: CRS; +} + +/** + * Represents a single resolution level in a raster tileset. + * + * COGs contain multiple resolution levels (overviews) for efficient + * visualization at different zoom levels. + * + * IMPORTANT: Overviews are ordered according to TileMatrixSet specification: + * - Index 0: Coarsest resolution (most zoomed out) + * - Index N: Finest resolution (most zoomed in) + * + * This matches the natural ordering where z increases with detail. + */ +export type TileMatrix = { + id: string; + + /** + * Scale denominator of this tile matrix. + * + * Defined as cellSize (meters per pixel) * meters per unit / 0.00028 + */ + scaleDenominator: number; + + /** + * Cell size of this tile matrix. + * + * This is the pixel size in meters. + */ + cellSize: number; + + // /** + // * Overview index in the TileMatrixSet ordering. + // * - Index 0: Coarsest resolution (most zoomed out) + // * - Higher indices: Progressively finer resolution + // * + // * This is the index in the COGMetadata.overviews array and represents + // * the natural ordering from coarse to fine. + // * + // * Note: This is different from GeoTIFF's internal level numbering where + // * level 0 is the full resolution image. + // * + // * @example + // * // For a COG with 4 resolutions: + // * index: 0 // Coarsest: 1250x1000 pixels (8x downsampled) + // * index: 1 // Medium: 2500x2000 pixels (4x downsampled) + // * index: 2 // Fine: 5000x4000 pixels (2x downsampled) + // * index: 3 // Finest: 10000x8000 pixels (full resolution) + // */ + // z: number; + + pointOfOrigin: Point; + + /** + * Width of each tile of this tile matrix in pixels. + */ + tileWidth: number; + /** + * Height of each tile of this tile matrix in pixels. + */ + tileHeight: number; + + /** + * Number of tiles in the X (horizontal) direction at this overview level. + * + * Calculated as: Math.ceil(width / tileWidth) + * + * @example + * // If tileWidth = 512: + * tilesX: 3 // z=0: ceil(1250 / 512) + * tilesX: 5 // z=1: ceil(2500 / 512) + * tilesX: 10 // z=2: ceil(5000 / 512) + * tilesX: 20 // z=3: ceil(10000 / 512) + */ + matrixWidth: number; + + /** + * Number of tiles in the Y (vertical) direction at this overview level. + * + * Calculated as: Math.ceil(height / tileHeight) + * + * @example + * // If tileHeight = 512: + * tilesY: 2 // z=0: ceil(1000 / 512) + * tilesY: 4 // z=1: ceil(2000 / 512) + * tilesY: 8 // z=2: ceil(4000 / 512) + * tilesY: 16 // z=3: ceil(8000 / 512) + */ + matrixHeight: number; + + // /** + // * Downsampling scale factor relative to full resolution (finest level). + // * + // * Indicates how much this overview is downsampled compared to the finest resolution. + // * - Scale factor of 1: Full resolution (finest level) + // * - Scale factor of 2: Half resolution + // * - Scale factor of 4: Quarter resolution + // * - Scale factor of 8: Eighth resolution (coarsest in this example) + // * + // * Common pattern: Each overview is 2x downsampled from the next finer level, + // * so scale factors are powers of 2: 8, 4, 2, 1 (from coarsest to finest) + // * + // * @example + // * scaleFactor: 8 // z=0: 1250x1000 (8x downsampled from finest) + // * scaleFactor: 4 // z=1: 2500x2000 (4x downsampled) + // * scaleFactor: 2 // z=2: 5000x4000 (2x downsampled) + // * scaleFactor: 1 // z=3: 10000x8000 (full resolution) + // */ + // scaleFactor: number; + + /** + * Affine geotransform for this overview level. + * + * Uses Python `affine` package ordering (NOT GDAL ordering): + * [a, b, c, d, e, f] where: + * - x_geo = a * col + b * row + c + * - y_geo = d * col + e * row + f + * + * Parameters: + * - a: pixel width (x resolution) + * - b: row rotation (typically 0) + * - c: x-coordinate of upper-left corner of the upper-left pixel + * - d: column rotation (typically 0) + * - e: pixel height (y resolution, typically negative) + * - f: y-coordinate of upper-left corner of the upper-left pixel + * + * @example + * // For a UTM image with 30m pixels: + * [30, 0, 440720, 0, -30, 3751320] + * // x_geo = 30 * col + 440720 + * // y_geo = -30 * row + 3751320 + */ + geotransform: [number, number, number, number, number, number]; +}; + +/** + * COG Metadata extracted from GeoTIFF + */ +export type TileMatrixSet = { + /** + * Title of this tile matrix set, normally used for display to a human + */ + title?: string; + /** + * Brief narrative description of this tile matrix set, normally available for display to a human + */ + description?: string; + crs: CRS; + + boundingBox?: TileMatrixSetBoundingBox; + /** + * Describes scale levels and its tile matrices + */ + tileMatrices: TileMatrix[]; + + projectToWgs84: (point: [number, number]) => [number, number]; + projectTo3857: (point: [number, number]) => [number, number]; +}; + +/** + * Raster Tile Index + * + * In TileMatrixSet ordering: `level === z`. + * + * So level `z` is the coarsest resolution (0) and the highest `z` is the finest + * resolution. + */ +export type TileIndex = { + x: number; + y: number; + + /** + * TileMatrixSet/OSM zoom (0 = coarsest, higher = finer) + */ + z: number; +}; From 4319a717bf60f5ff1dc03e937f9a1f97561ea80b Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 15 Dec 2025 19:52:01 -0500 Subject: [PATCH 12/12] Don't check between x and y scale --- packages/deck.gl-cog/src/cog-tile-matrix-set.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts index 3db11747..20b03671 100644 --- a/packages/deck.gl-cog/src/cog-tile-matrix-set.ts +++ b/packages/deck.gl-cog/src/cog-tile-matrix-set.ts @@ -154,7 +154,6 @@ function createOverviewTileMatrix({ id, image, fullWidth, - fullHeight, baseTransform, parsedCrs, }: { @@ -168,12 +167,14 @@ function createOverviewTileMatrix({ const width = image.getWidth(); const height = image.getHeight(); + // For now, just use scaleX + // https://github.com/developmentseed/morecantile/pull/187/changes#r2621314673 const scaleX = fullWidth / width; - const scaleY = fullHeight / height; - if (Math.abs(scaleX - scaleY) > 1e-6) { - throw new Error("Non-uniform overview scaling detected (X/Y differ)"); - } + // const scaleY = fullHeight / height; + // if (Math.abs(scaleX - scaleY) > 1e-3) { + // throw new Error("Non-uniform overview scaling detected (X/Y differ)"); + // } const scale = scaleX;