From 4cc6a2702680231085d64c4e60e70d47327bdce7 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 24 Feb 2026 05:35:50 -0500 Subject: [PATCH 01/16] feat(wip): embeddings visualization --- examples/embeddings/README.md | 0 examples/embeddings/flip.py | 24 ++++ examples/embeddings/index.html | 22 +++ examples/embeddings/package.json | 35 +++++ examples/embeddings/src/App.tsx | 157 ++++++++++++++++++++++ examples/embeddings/src/main.tsx | 9 ++ examples/embeddings/tsconfig.json | 24 ++++ examples/embeddings/vite.config.ts | 10 ++ packages/deck.gl-geotiff/package.json | 2 +- packages/deck.gl-geotiff/src/cog-layer.ts | 8 +- packages/deck.gl-geotiff/src/index.ts | 2 +- packages/geotiff/package.json | 3 +- packages/geotiff/src/codecs/zstd.ts | 5 + packages/geotiff/src/decode.ts | 6 +- packages/geotiff/src/fetch.ts | 48 ++++++- packages/geotiff/src/geotiff.ts | 2 +- packages/geotiff/src/ifd.ts | 14 +- packages/geotiff/src/index.ts | 3 +- packages/geotiff/src/overview.ts | 4 +- pnpm-lock.yaml | 86 +++++++++++- 20 files changed, 437 insertions(+), 27 deletions(-) create mode 100644 examples/embeddings/README.md create mode 100644 examples/embeddings/flip.py create mode 100644 examples/embeddings/index.html create mode 100644 examples/embeddings/package.json create mode 100644 examples/embeddings/src/App.tsx create mode 100644 examples/embeddings/src/main.tsx create mode 100644 examples/embeddings/tsconfig.json create mode 100644 examples/embeddings/vite.config.ts create mode 100644 packages/geotiff/src/codecs/zstd.ts diff --git a/examples/embeddings/README.md b/examples/embeddings/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/embeddings/flip.py b/examples/embeddings/flip.py new file mode 100644 index 00000000..75da9c38 --- /dev/null +++ b/examples/embeddings/flip.py @@ -0,0 +1,24 @@ +import rasterio +from rasterio.transform import Affine + +with rasterio.open("public/xjejfvrbm1fbu1ecw-0000000000-0000008192.tiff") as src: + data = src.read() + # Flip all bands on the y-axis + data = data[:, ::-1, :] + + transform = src.transform + # Negate the y pixel size and adjust the origin to the top-left + new_transform = Affine( + transform.a, + transform.b, + transform.c, + transform.d, + -transform.e, + transform.f + transform.e * src.height, + ) + + profile = src.profile.copy() + profile.update(transform=new_transform) + + with rasterio.open("flipped.tif", "w", **profile) as dst: + dst.write(data) diff --git a/examples/embeddings/index.html b/examples/embeddings/index.html new file mode 100644 index 00000000..87c554fc --- /dev/null +++ b/examples/embeddings/index.html @@ -0,0 +1,22 @@ + + + + + + Embeddings Example + + + +
+ + + diff --git a/examples/embeddings/package.json b/examples/embeddings/package.json new file mode 100644 index 00000000..883f49b3 --- /dev/null +++ b/examples/embeddings/package.json @@ -0,0 +1,35 @@ +{ + "name": "deck.gl-embeddings-example", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "publish": "pnpm build && gh-pages -d dist -b gh-pages -e examples/embeddings" + }, + "dependencies": { + "@deck.gl/core": "^9.2.7", + "@deck.gl/geo-layers": "^9.2.7", + "@deck.gl/layers": "^9.2.7", + "@deck.gl/mapbox": "^9.2.7", + "@deck.gl/mesh-layers": "^9.2.7", + "@developmentseed/geotiff": "workspace:^", + "@developmentseed/deck.gl-geotiff": "workspace:^", + "@developmentseed/deck.gl-raster": "workspace:^", + "@luma.gl/core": "9.2.6", + "@luma.gl/shadertools": "9.2.6", + "maplibre-gl": "^5.17.0", + "proj4": "^2.20.2", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-map-gl": "^8.1.0" + }, + "devDependencies": { + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "gh-pages": "^6.3.0", + "vite": "^7.3.1" + } +} diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx new file mode 100644 index 00000000..079a6dda --- /dev/null +++ b/examples/embeddings/src/App.tsx @@ -0,0 +1,157 @@ +import type { DeckProps } from "@deck.gl/core"; +import { MapboxOverlay } from "@deck.gl/mapbox"; +import { COGLayer } from "@developmentseed/deck.gl-geotiff"; +import type { GetTileDataOptions } from "@developmentseed/deck.gl-geotiff"; +import type { RasterModule } from "@developmentseed/deck.gl-raster"; +import { CreateTexture } from "@developmentseed/deck.gl-raster/gpu-modules"; +import type { GeoTIFF, Overview } from "@developmentseed/geotiff"; +import type { Device } from "@luma.gl/core"; +import { useCallback, useState } from "react"; +import "maplibre-gl/dist/maplibre-gl.css"; +import { Map as MaplibreMap, useControl } from "react-map-gl/maplibre"; + +const NUM_BANDS = 64; +const COG_URL = "flipped.tif"; + +type TileData = { + device: Device; + data: Uint8Array; + height: number; + width: number; +}; + +function DeckGLOverlay(props: DeckProps) { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +function makeTileDataFetcher(bands: [number, number, number]) { + return async function getTileData( + image: GeoTIFF | Overview, + options: GetTileDataOptions, + ): Promise { + const { device, x, y, signal } = options; + const tiles = await Promise.all( + bands.map((b) => + image.fetchTile(x, y, { signal, boundless: false, band: b }), + ), + ); + + const { width, height } = tiles[0]!.array; + const pixelCount = width * height; + const uint8Data = new Uint8Array(pixelCount * 4); + + for (let i = 0; i < pixelCount; i++) { + const outBase = i * 4; + for (let c = 0; c < 3; c++) { + const tile = tiles[c]!; + const value = + tile.array.layout === "pixel-interleaved" + ? (tile.array.data[i] as number) + : (tile.array.bands[0]![i] as number); + uint8Data[outBase + c] = value + 128; + } + uint8Data[outBase + 3] = 255; + } + + return { device, data: uint8Data, height, width }; + }; +} + +function renderTile(data: TileData): RasterModule[] { + const { device, data: uint8Data, height, width } = data; + const texture = device.createTexture({ + data: uint8Data, + format: "rgba8unorm", + width, + height, + sampler: { magFilter: "nearest", minFilter: "nearest" }, + }); + return [{ module: CreateTexture, props: { textureName: texture } }]; +} + +function BandSelector({ + label, + value, + onChange, +}: { + label: string; + value: number; + onChange: (v: number) => void; +}) { + return ( + + ); +} + +export default function App() { + const [r, setR] = useState(0); + const [g, setG] = useState(1); + const [b, setB] = useState(2); + + const getTileData = useCallback( + () => makeTileDataFetcher([r, g, b]), + [r, g, b], + ); + + const layer = new COGLayer({ + id: `embeddings-layer-${r}-${g}-${b}`, + geotiff: COG_URL, + getTileData: getTileData(), + renderTile, + }); + + return ( +
+ + + +
+ + + +
+
+ ); +} diff --git a/examples/embeddings/src/main.tsx b/examples/embeddings/src/main.tsx new file mode 100644 index 00000000..f8fc6f51 --- /dev/null +++ b/examples/embeddings/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/examples/embeddings/tsconfig.json b/examples/embeddings/tsconfig.json new file mode 100644 index 00000000..f0a23505 --- /dev/null +++ b/examples/embeddings/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/embeddings/vite.config.ts b/examples/embeddings/vite.config.ts new file mode 100644 index 00000000..b40ddcc0 --- /dev/null +++ b/examples/embeddings/vite.config.ts @@ -0,0 +1,10 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + base: "/deck.gl-raster/examples/embeddings/", + server: { + port: 3001, + }, +}); diff --git a/packages/deck.gl-geotiff/package.json b/packages/deck.gl-geotiff/package.json index 5023b809..9081bef1 100644 --- a/packages/deck.gl-geotiff/package.json +++ b/packages/deck.gl-geotiff/package.json @@ -46,7 +46,7 @@ "vitest": "^4.0.18" }, "dependencies": { - "@cogeotiff/core": "^9.3.0", + "@cogeotiff/core": "file:/Users/gadomski/Code/blacha/cogeotiff/packages/core", "@developmentseed/affine": "workspace:^", "@developmentseed/deck.gl-raster": "workspace:^", "@developmentseed/geotiff": "workspace:^", diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index c89ed8e3..0d511bb9 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -249,8 +249,12 @@ export class COGLayer< }); } - const { getTileData: defaultGetTileData, renderTile: defaultRenderTile } = - inferRenderPipeline(geotiff, this.context.device); + let defaultGetTileData: COGLayerProps["getTileData"] | undefined; + let defaultRenderTile: COGLayerProps["renderTile"] | undefined; + if (!this.props.getTileData || !this.props.renderTile) { + ({ getTileData: defaultGetTileData, renderTile: defaultRenderTile } = + inferRenderPipeline(geotiff, this.context.device)); + } this.setState({ geotiff, diff --git a/packages/deck.gl-geotiff/src/index.ts b/packages/deck.gl-geotiff/src/index.ts index da537d24..ae5d0431 100644 --- a/packages/deck.gl-geotiff/src/index.ts +++ b/packages/deck.gl-geotiff/src/index.ts @@ -1,4 +1,4 @@ -export type { COGLayerProps } from "./cog-layer.js"; +export type { COGLayerProps, GetTileDataOptions } from "./cog-layer.js"; export { COGLayer } from "./cog-layer.js"; export * as texture from "./geotiff/texture.js"; // Don't export GeoTIFF Layer for now; nudge people towards COGLayer diff --git a/packages/geotiff/package.json b/packages/geotiff/package.json index c1989468..ce78aa05 100644 --- a/packages/geotiff/package.json +++ b/packages/geotiff/package.json @@ -51,10 +51,11 @@ "@chunkd/source": "^11.1.0", "@chunkd/source-http": "^11.1.1", "@chunkd/source-memory": "^11.0.2", - "@cogeotiff/core": "^9.3.0", + "@cogeotiff/core": "file:/Users/gadomski/Code/blacha/cogeotiff/packages/core", "@developmentseed/affine": "workspace:^", "@developmentseed/lzw-tiff-decoder": "^0.2.2", "@developmentseed/morecantile": "workspace:^", + "fzstd": "^0.1.1", "uuid": "^13.0.0" } } diff --git a/packages/geotiff/src/codecs/zstd.ts b/packages/geotiff/src/codecs/zstd.ts new file mode 100644 index 00000000..4ddbd7c3 --- /dev/null +++ b/packages/geotiff/src/codecs/zstd.ts @@ -0,0 +1,5 @@ +import { decompress } from "fzstd"; + +export async function decode(bytes: ArrayBuffer): Promise { + return decompress(new Uint8Array(bytes)).buffer as ArrayBuffer; +} diff --git a/packages/geotiff/src/decode.ts b/packages/geotiff/src/decode.ts index c92a1493..8420e307 100644 --- a/packages/geotiff/src/decode.ts +++ b/packages/geotiff/src/decode.ts @@ -46,9 +46,9 @@ registry.set(Compression.DeflateOther, () => registry.set(Compression.Lzw, () => import("./codecs/lzw.js").then((m) => m.decode), ); -// registry.set(Compression.Zstd, () => -// import("../codecs/zstd.js").then((m) => m.decode), -// ); +registry.set(Compression.Zstd, () => + import("./codecs/zstd.js").then((m) => m.decode), +); // registry.set(Compression.Lzma, () => // import("../codecs/lzma.js").then((m) => m.decode), // ); diff --git a/packages/geotiff/src/fetch.ts b/packages/geotiff/src/fetch.ts index bc2a786f..c14a5b63 100644 --- a/packages/geotiff/src/fetch.ts +++ b/packages/geotiff/src/fetch.ts @@ -1,5 +1,5 @@ import type { SampleFormat, TiffImage } from "@cogeotiff/core"; -import { TiffTag } from "@cogeotiff/core"; +import { PlanarConfiguration, TiffTag } from "@cogeotiff/core"; import { compose, translation } from "@developmentseed/affine"; import type { RasterArray } from "./array.js"; import type { ProjJson } from "./crs.js"; @@ -35,13 +35,13 @@ export async function fetchTile( self: HasTiffReference, x: number, y: number, - options: { boundless?: boolean; signal?: AbortSignal } = {}, + options: { boundless?: boolean; signal?: AbortSignal; band?: number } = {}, ): Promise { if (self.maskImage != null) { throw new Error("Mask fetching not implemented yet"); } - const tile = await self.image.getTile(x, y, options); + const tile = await getTileForBand(self.image, x, y, options.band, options); if (tile === null) { throw new Error("Tile not found"); } @@ -65,10 +65,14 @@ export async function fetchTile( const samplesPerPixel = self.image.value(TiffTag.SamplesPerPixel) ?? 1; + // For band-separate images, each tile contains a single band's data. + const isBandSeparate = planarConfiguration === PlanarConfiguration.Separate; + const tileSamplesPerPixel = isBandSeparate ? 1 : samplesPerPixel; + const decodedPixels = await decode(bytes, compression, { sampleFormat, bitsPerSample, - samplesPerPixel, + samplesPerPixel: tileSamplesPerPixel, width: self.tileWidth, height: self.tileHeight, predictor, @@ -77,7 +81,7 @@ export async function fetchTile( const array: RasterArray = { ...decodedPixels, - count: samplesPerPixel, + count: tileSamplesPerPixel, height: self.tileHeight, width: self.tileWidth, mask: null, @@ -168,6 +172,40 @@ function clipToImageBounds( }; } +/** + * Fetch a tile, optionally for a specific band in band-separate images. + * + * For band-separate (PlanarConfiguration=Separate) TIFFs, tile offsets are + * laid out as all spatial tiles for band 0, then band 1, etc. cogeotiff's + * `getTile(x, y)` always fetches band 0. This helper computes the correct + * flat tile index to fetch an arbitrary band. + */ +async function getTileForBand( + image: TiffImage, + x: number, + y: number, + band: number | undefined, + options?: { signal?: AbortSignal }, +) { + if (band == null || band === 0) { + return image.getTile(x, y, options); + } + + const size = image.size; + const tiles = image.tileSize; + if (tiles == null) { + throw new Error("Tiff is not tiled"); + } + + const nxTiles = Math.ceil(size.width / tiles.width); + const nyTiles = Math.ceil(size.height / tiles.height); + const spatialTileCount = nxTiles * nyTiles; + const idx = band * spatialTileCount + y * nxTiles + x; + + const { offset, imageSize } = await image.getTileSize(idx); + return image.getBytes(offset, imageSize, options); +} + function getUniqueSampleFormat( sampleFormats: SampleFormat[], bitsPerSamples: Uint16Array, diff --git a/packages/geotiff/src/geotiff.ts b/packages/geotiff/src/geotiff.ts index 01a67250..3ab53476 100644 --- a/packages/geotiff/src/geotiff.ts +++ b/packages/geotiff/src/geotiff.ts @@ -279,7 +279,7 @@ export class GeoTIFF { async fetchTile( x: number, y: number, - options: { boundless?: boolean; signal?: AbortSignal } = {}, + options: { boundless?: boolean; signal?: AbortSignal; band?: number } = {}, ): Promise { return await fetchTile(this, x, y, options); } diff --git a/packages/geotiff/src/ifd.ts b/packages/geotiff/src/ifd.ts index 1c1508cc..bd718926 100644 --- a/packages/geotiff/src/ifd.ts +++ b/packages/geotiff/src/ifd.ts @@ -1,5 +1,10 @@ import type { TiffImage, TiffTagGeoType, TiffTagType } from "@cogeotiff/core"; -import { Predictor, SampleFormat, TiffTag, TiffTagGeo } from "@cogeotiff/core"; +import { + Predictor, + SampleFormat, + TiffTag, + TiffTagGeo, +} from "@cogeotiff/core"; /** Subset of TIFF tags that we pre-fetch for easier visualization. */ export interface CachedTags { @@ -23,8 +28,6 @@ export async function prefetchTags(image: TiffImage): Promise { throw new Error("Compression tag should always exist."); } - const nodata = image.noData; - const [ bitsPerSample, colorMap, @@ -41,8 +44,13 @@ export async function prefetchTags(image: TiffImage): Promise { image.fetch(TiffTag.Predictor), image.fetch(TiffTag.SampleFormat), image.fetch(TiffTag.SamplesPerPixel), + image.fetch(TiffTag.GdalNoData), ]); + // Access noData after fetching the GdalNoData tag, since the getter is + // synchronous and throws if the tag exists but hasn't been loaded yet. + const nodata = image.noData; + const missingTag: (tagName: string) => never = (tagName: string) => { throw new Error(`${tagName} tag should always exist.`); }; diff --git a/packages/geotiff/src/index.ts b/packages/geotiff/src/index.ts index 9ea92b42..746f829f 100644 --- a/packages/geotiff/src/index.ts +++ b/packages/geotiff/src/index.ts @@ -1,4 +1,5 @@ -export type { RasterArray } from "./array.js"; +export type { PixelRasterArray, RasterArray } from "./array.js"; +export { packBandsToRGBA } from "./array.js"; export { parseColormap } from "./colormap.js"; export type { ProjJson } from "./crs.js"; export type { DecodedPixels, Decoder, DecoderMetadata } from "./decode.js"; diff --git a/packages/geotiff/src/overview.ts b/packages/geotiff/src/overview.ts index c65cbf12..c70cb037 100644 --- a/packages/geotiff/src/overview.ts +++ b/packages/geotiff/src/overview.ts @@ -79,11 +79,11 @@ export class Overview { return this.image.size.width; } - /** Fetch a single tile from the full-resolution image. */ + /** Fetch a single tile from this overview level. */ async fetchTile( x: number, y: number, - options: { boundless?: boolean; signal?: AbortSignal } = {}, + options: { boundless?: boolean; signal?: AbortSignal; band?: number } = {}, ): Promise { return await fetchTile(this, x, y, options); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c62c2090..48b55cc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,6 +102,70 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.1.0)(tsx@4.21.0) + examples/embeddings: + dependencies: + '@deck.gl/core': + specifier: ^9.2.8 + version: 9.2.8 + '@deck.gl/geo-layers': + specifier: ^9.2.8 + version: 9.2.8(@deck.gl/core@9.2.8)(@deck.gl/extensions@9.2.5(@deck.gl/core@9.2.8)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))))(@deck.gl/layers@9.2.8(@deck.gl/core@9.2.8)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))))(@deck.gl/mesh-layers@9.2.8(@deck.gl/core@9.2.8)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/gltf@9.2.6(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@loaders.gl/core@4.3.4)(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))) + '@deck.gl/layers': + specifier: ^9.2.8 + version: 9.2.8(@deck.gl/core@9.2.8)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))) + '@deck.gl/mapbox': + specifier: ^9.2.8 + version: 9.2.8(@deck.gl/core@9.2.8)(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@math.gl/web-mercator@4.1.0) + '@deck.gl/mesh-layers': + specifier: ^9.2.8 + version: 9.2.8(@deck.gl/core@9.2.8)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/gltf@9.2.6(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)) + '@developmentseed/deck.gl-geotiff': + specifier: workspace:^ + version: link:../../packages/deck.gl-geotiff + '@developmentseed/deck.gl-raster': + specifier: workspace:^ + version: link:../../packages/deck.gl-raster + '@developmentseed/geotiff': + specifier: workspace:^ + version: link:../../packages/geotiff + '@luma.gl/core': + specifier: ^9.2.6 + version: 9.2.6 + '@luma.gl/shadertools': + specifier: ^9.2.6 + version: 9.2.6(@luma.gl/core@9.2.6) + maplibre-gl: + specifier: ^5.17.0 + version: 5.17.0 + proj4: + specifier: ^2.20.2 + version: 2.20.2 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-map-gl: + specifier: ^8.1.0 + version: 8.1.0(maplibre-gl@5.17.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + devDependencies: + '@types/react': + specifier: ^19.2.10 + version: 19.2.10 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.10) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.2(vite@7.3.1(@types/node@25.1.0)(tsx@4.21.0)) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.1.0)(tsx@4.21.0) + examples/land-cover: dependencies: '@deck.gl/core': @@ -245,8 +309,8 @@ importers: packages/deck.gl-geotiff: dependencies: '@cogeotiff/core': - specifier: ^9.3.0 - version: 9.3.0 + specifier: file:/Users/gadomski/Code/blacha/cogeotiff/packages/core + version: file:../../blacha/cogeotiff/packages/core '@deck.gl/core': specifier: ^9.2.8 version: 9.2.8 @@ -395,8 +459,8 @@ importers: specifier: ^11.0.2 version: 11.0.2 '@cogeotiff/core': - specifier: ^9.3.0 - version: 9.3.0 + specifier: file:/Users/gadomski/Code/blacha/cogeotiff/packages/core + version: file:../../blacha/cogeotiff/packages/core '@developmentseed/affine': specifier: workspace:^ version: link:../affine @@ -406,6 +470,9 @@ importers: '@developmentseed/morecantile': specifier: workspace:^ version: link:../morecantile + fzstd: + specifier: ^0.1.1 + version: 0.1.1 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -652,8 +719,8 @@ packages: resolution: {integrity: sha512-3BcrHK4XDm3t4vDx1OisgcOZ05+EarEqwaGVIL7IMHUmkXwN/Aprlnr7aVP3BYcltgcCcfMd1pXQicbSrP563g==} engines: {node: '>=16.0.0'} - '@cogeotiff/core@9.3.0': - resolution: {integrity: sha512-TrJ0s/MwNxEUym2uRHOrNh6gF50zhfiNvdAdi3Ec7tNFWmdw4CKQqbYtHIryOunUG18xa1senLUIGB3N+pCBGg==} + '@cogeotiff/core@file:../../blacha/cogeotiff/packages/core': + resolution: {directory: ../../blacha/cogeotiff/packages/core, type: directory} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} '@csstools/color-helpers@5.1.0': @@ -1798,6 +1865,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fzstd@0.1.1: + resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -2839,7 +2909,7 @@ snapshots: '@chunkd/source@11.1.0': {} - '@cogeotiff/core@9.3.0': {} + '@cogeotiff/core@file:../../blacha/cogeotiff/packages/core': {} '@csstools/color-helpers@5.1.0': {} @@ -3962,6 +4032,8 @@ snapshots: fsevents@2.3.3: optional: true + fzstd@0.1.1: {} + gensync@1.0.0-beta.2: {} geotiff-geokeys-to-proj4@2024.4.13: {} From 478d707cfd69edbe6632815abf1eb95688c2ec75 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 24 Feb 2026 06:14:32 -0500 Subject: [PATCH 02/16] fix; env vars --- examples/embeddings/src/App.tsx | 2 +- examples/embeddings/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx index 079a6dda..21c58456 100644 --- a/examples/embeddings/src/App.tsx +++ b/examples/embeddings/src/App.tsx @@ -11,7 +11,7 @@ import "maplibre-gl/dist/maplibre-gl.css"; import { Map as MaplibreMap, useControl } from "react-map-gl/maplibre"; const NUM_BANDS = 64; -const COG_URL = "flipped.tif"; +const COG_URL = import.meta.env.VITE_COG_URL ?? "flipped.tif"; type TileData = { device: Device; diff --git a/examples/embeddings/vite.config.ts b/examples/embeddings/vite.config.ts index b40ddcc0..10419b86 100644 --- a/examples/embeddings/vite.config.ts +++ b/examples/embeddings/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], - base: "/deck.gl-raster/examples/embeddings/", + base: process.env.VITE_BASE ?? "/deck.gl-raster/examples/embeddings/", server: { port: 3001, }, From 7bf691c4ef550e1b4ac4ba48117a3911a3598d35 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Fri, 27 Feb 2026 11:59:47 -0500 Subject: [PATCH 03/16] fix: update submodules --- fixtures/geotiff-test-data | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/geotiff-test-data b/fixtures/geotiff-test-data index 4f268831..ee231aa8 160000 --- a/fixtures/geotiff-test-data +++ b/fixtures/geotiff-test-data @@ -1 +1 @@ -Subproject commit 4f2688310cbd9fccdd1c677ed8057b829ce99cb2 +Subproject commit ee231aa8e99a433987c40da96806d446d59437c4 From 62811eaa0cb467a1a6c88004acc9b7bb428621ba Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Fri, 27 Feb 2026 12:01:28 -0500 Subject: [PATCH 04/16] fix: dependencies --- packages/deck.gl-geotiff/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/deck.gl-geotiff/package.json b/packages/deck.gl-geotiff/package.json index 9081bef1..694fcc88 100644 --- a/packages/deck.gl-geotiff/package.json +++ b/packages/deck.gl-geotiff/package.json @@ -46,7 +46,7 @@ "vitest": "^4.0.18" }, "dependencies": { - "@cogeotiff/core": "file:/Users/gadomski/Code/blacha/cogeotiff/packages/core", + "@cogeotiff/core": "^9.4.0", "@developmentseed/affine": "workspace:^", "@developmentseed/deck.gl-raster": "workspace:^", "@developmentseed/geotiff": "workspace:^", From 5e8577c59b722177f429b122b4dede7034f7f063 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Fri, 27 Feb 2026 12:13:50 -0500 Subject: [PATCH 05/16] fix: remove stuff --- examples/embeddings/flip.py | 24 ------------------------ examples/embeddings/src/App.tsx | 3 ++- pnpm-lock.yaml | 10 ++++++++-- 3 files changed, 10 insertions(+), 27 deletions(-) delete mode 100644 examples/embeddings/flip.py diff --git a/examples/embeddings/flip.py b/examples/embeddings/flip.py deleted file mode 100644 index 75da9c38..00000000 --- a/examples/embeddings/flip.py +++ /dev/null @@ -1,24 +0,0 @@ -import rasterio -from rasterio.transform import Affine - -with rasterio.open("public/xjejfvrbm1fbu1ecw-0000000000-0000008192.tiff") as src: - data = src.read() - # Flip all bands on the y-axis - data = data[:, ::-1, :] - - transform = src.transform - # Negate the y pixel size and adjust the origin to the top-left - new_transform = Affine( - transform.a, - transform.b, - transform.c, - transform.d, - -transform.e, - transform.f + transform.e * src.height, - ) - - profile = src.profile.copy() - profile.update(transform=new_transform) - - with rasterio.open("flipped.tif", "w", **profile) as dst: - dst.write(data) diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx index 21c58456..aa94a11b 100644 --- a/examples/embeddings/src/App.tsx +++ b/examples/embeddings/src/App.tsx @@ -11,7 +11,8 @@ import "maplibre-gl/dist/maplibre-gl.css"; import { Map as MaplibreMap, useControl } from "react-map-gl/maplibre"; const NUM_BANDS = 64; -const COG_URL = import.meta.env.VITE_COG_URL ?? "flipped.tif"; +const COG_URL = + "https://data.source.coop/tge-labs/aef/v1/annual/2024/13N/x2ui4lsatulad51m6-0000008192-0000008192.tiff"; type TileData = { device: Device; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b55cc2..08828bbb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,8 +309,8 @@ importers: packages/deck.gl-geotiff: dependencies: '@cogeotiff/core': - specifier: file:/Users/gadomski/Code/blacha/cogeotiff/packages/core - version: file:../../blacha/cogeotiff/packages/core + specifier: ^9.4.0 + version: 9.4.0 '@deck.gl/core': specifier: ^9.2.8 version: 9.2.8 @@ -719,6 +719,10 @@ packages: resolution: {integrity: sha512-3BcrHK4XDm3t4vDx1OisgcOZ05+EarEqwaGVIL7IMHUmkXwN/Aprlnr7aVP3BYcltgcCcfMd1pXQicbSrP563g==} engines: {node: '>=16.0.0'} + '@cogeotiff/core@9.4.0': + resolution: {integrity: sha512-2aI7NHN2XMLTJx4LXghhYY2jMyJLJe4rnWTmnA6RuwaYjiZPhLc6PSmzNcaOebtraklucmJHXgfhTi3s+/XVaQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + '@cogeotiff/core@file:../../blacha/cogeotiff/packages/core': resolution: {directory: ../../blacha/cogeotiff/packages/core, type: directory} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2909,6 +2913,8 @@ snapshots: '@chunkd/source@11.1.0': {} + '@cogeotiff/core@9.4.0': {} + '@cogeotiff/core@file:../../blacha/cogeotiff/packages/core': {} '@csstools/color-helpers@5.1.0': {} From 6e045b8afdf6a3840d26e2615b87ab75fde353b7 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Fri, 27 Feb 2026 12:26:01 -0500 Subject: [PATCH 06/16] fix: url --- examples/embeddings/src/App.tsx | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx index aa94a11b..f5a9cb0e 100644 --- a/examples/embeddings/src/App.tsx +++ b/examples/embeddings/src/App.tsx @@ -11,8 +11,9 @@ import "maplibre-gl/dist/maplibre-gl.css"; import { Map as MaplibreMap, useControl } from "react-map-gl/maplibre"; const NUM_BANDS = 64; -const COG_URL = - "https://data.source.coop/tge-labs/aef/v1/annual/2024/13N/x2ui4lsatulad51m6-0000008192-0000008192.tiff"; +const DEFAULT_COG_URL = + // "https://data.source.coop/tge-labs/aef/v1/annual/2024/13N/xjejfvrbm1fbu1ecw-0000000000-0000008192.tiff"; + "http://devseed-gadomski-demo.s3-website-us-east-1.amazonaws.com/xjejfvrbm1fbu1ecw-0000000000-0000008192.flipped.tif"; type TileData = { device: Device; @@ -104,6 +105,7 @@ function BandSelector({ } export default function App() { + const [cogUrl, setCogUrl] = useState(DEFAULT_COG_URL); const [r, setR] = useState(0); const [g, setG] = useState(1); const [b, setB] = useState(2); @@ -114,8 +116,8 @@ export default function App() { ); const layer = new COGLayer({ - id: `embeddings-layer-${r}-${g}-${b}`, - geotiff: COG_URL, + id: `embeddings-layer-${cogUrl}-${r}-${g}-${b}`, + geotiff: cogUrl, getTileData: getTileData(), renderTile, }); @@ -149,6 +151,21 @@ export default function App() { gap: 6, }} > + setCogUrl(e.target.value)} + placeholder="COG URL" + style={{ + width: 360, + padding: "4px 6px", + fontSize: 13, + background: "rgba(255,255,255,0.1)", + color: "#fff", + border: "1px solid rgba(255,255,255,0.3)", + borderRadius: 4, + }} + /> From 70c06407fcd57f0887ac5c9631a613c9046ea88d Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 08:40:29 -0700 Subject: [PATCH 07/16] fix: formatting --- packages/deck.gl-geotiff/src/cog-layer.ts | 8 ++++++-- packages/geotiff/src/ifd.ts | 7 +------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 0d511bb9..c2ab9610 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -249,8 +249,12 @@ export class COGLayer< }); } - let defaultGetTileData: COGLayerProps["getTileData"] | undefined; - let defaultRenderTile: COGLayerProps["renderTile"] | undefined; + let defaultGetTileData: + | COGLayerProps["getTileData"] + | undefined; + let defaultRenderTile: + | COGLayerProps["renderTile"] + | undefined; if (!this.props.getTileData || !this.props.renderTile) { ({ getTileData: defaultGetTileData, renderTile: defaultRenderTile } = inferRenderPipeline(geotiff, this.context.device)); diff --git a/packages/geotiff/src/ifd.ts b/packages/geotiff/src/ifd.ts index 0b2b7e79..7f4f7586 100644 --- a/packages/geotiff/src/ifd.ts +++ b/packages/geotiff/src/ifd.ts @@ -1,10 +1,5 @@ import type { TiffImage, TiffTagGeoType, TiffTagType } from "@cogeotiff/core"; -import { - Predictor, - SampleFormat, - TiffTag, - TiffTagGeo, -} from "@cogeotiff/core"; +import { Predictor, SampleFormat, TiffTag, TiffTagGeo } from "@cogeotiff/core"; /** Subset of TIFF tags that we pre-fetch for easier visualization. */ export interface CachedTags { From 4b197064681b34bc279a1751a0449085de302e32 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 08:47:20 -0700 Subject: [PATCH 08/16] fix: import --- examples/embeddings/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx index f5a9cb0e..8bb0b095 100644 --- a/examples/embeddings/src/App.tsx +++ b/examples/embeddings/src/App.tsx @@ -1,7 +1,7 @@ import type { DeckProps } from "@deck.gl/core"; import { MapboxOverlay } from "@deck.gl/mapbox"; -import { COGLayer } from "@developmentseed/deck.gl-geotiff"; import type { GetTileDataOptions } from "@developmentseed/deck.gl-geotiff"; +import { COGLayer } from "@developmentseed/deck.gl-geotiff"; import type { RasterModule } from "@developmentseed/deck.gl-raster"; import { CreateTexture } from "@developmentseed/deck.gl-raster/gpu-modules"; import type { GeoTIFF, Overview } from "@developmentseed/geotiff"; From 12c693a26536ad71fe35b14e769e2f13d2a20baa Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 09:20:16 -0700 Subject: [PATCH 09/16] fix: cache bands --- examples/embeddings/src/App.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/embeddings/src/App.tsx b/examples/embeddings/src/App.tsx index 8bb0b095..5b3e3a65 100644 --- a/examples/embeddings/src/App.tsx +++ b/examples/embeddings/src/App.tsx @@ -28,6 +28,9 @@ function DeckGLOverlay(props: DeckProps) { return null; } +type FetchedTile = Awaited>; +const bandCache = new Map(); + function makeTileDataFetcher(bands: [number, number, number]) { return async function getTileData( image: GeoTIFF | Overview, @@ -35,9 +38,18 @@ function makeTileDataFetcher(bands: [number, number, number]) { ): Promise { const { device, x, y, signal } = options; const tiles = await Promise.all( - bands.map((b) => - image.fetchTile(x, y, { signal, boundless: false, band: b }), - ), + bands.map((b) => { + const key = `${x}-${y}-${b}`; + const cached = bandCache.get(key); + if (cached) return cached; + const result = image + .fetchTile(x, y, { signal, boundless: false, band: b }) + .then((tile) => { + bandCache.set(key, tile); + return tile; + }); + return result; + }), ); const { width, height } = tiles[0]!.array; From dbbc81fb115dd8d2db6bf4e6a9994e2490fe60e9 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 09:28:43 -0700 Subject: [PATCH 10/16] refactor: rename to aef-embeddings --- .../{embeddings => aef-embeddings}/README.md | 0 .../{embeddings => aef-embeddings}/index.html | 0 .../package.json | 2 +- .../src/App.tsx | 0 .../src/main.tsx | 0 .../tsconfig.json | 0 .../vite.config.ts | 2 +- pnpm-lock.yaml | 40 +++++++++---------- 8 files changed, 22 insertions(+), 22 deletions(-) rename examples/{embeddings => aef-embeddings}/README.md (100%) rename examples/{embeddings => aef-embeddings}/index.html (100%) rename examples/{embeddings => aef-embeddings}/package.json (97%) rename examples/{embeddings => aef-embeddings}/src/App.tsx (100%) rename examples/{embeddings => aef-embeddings}/src/main.tsx (100%) rename examples/{embeddings => aef-embeddings}/tsconfig.json (100%) rename examples/{embeddings => aef-embeddings}/vite.config.ts (68%) diff --git a/examples/embeddings/README.md b/examples/aef-embeddings/README.md similarity index 100% rename from examples/embeddings/README.md rename to examples/aef-embeddings/README.md diff --git a/examples/embeddings/index.html b/examples/aef-embeddings/index.html similarity index 100% rename from examples/embeddings/index.html rename to examples/aef-embeddings/index.html diff --git a/examples/embeddings/package.json b/examples/aef-embeddings/package.json similarity index 97% rename from examples/embeddings/package.json rename to examples/aef-embeddings/package.json index 883f49b3..21ab1594 100644 --- a/examples/embeddings/package.json +++ b/examples/aef-embeddings/package.json @@ -6,7 +6,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "publish": "pnpm build && gh-pages -d dist -b gh-pages -e examples/embeddings" + "publish": "pnpm build && gh-pages -d dist -b gh-pages -e examples/aef-embeddings" }, "dependencies": { "@deck.gl/core": "^9.2.7", diff --git a/examples/embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx similarity index 100% rename from examples/embeddings/src/App.tsx rename to examples/aef-embeddings/src/App.tsx diff --git a/examples/embeddings/src/main.tsx b/examples/aef-embeddings/src/main.tsx similarity index 100% rename from examples/embeddings/src/main.tsx rename to examples/aef-embeddings/src/main.tsx diff --git a/examples/embeddings/tsconfig.json b/examples/aef-embeddings/tsconfig.json similarity index 100% rename from examples/embeddings/tsconfig.json rename to examples/aef-embeddings/tsconfig.json diff --git a/examples/embeddings/vite.config.ts b/examples/aef-embeddings/vite.config.ts similarity index 68% rename from examples/embeddings/vite.config.ts rename to examples/aef-embeddings/vite.config.ts index 10419b86..8ff42aec 100644 --- a/examples/embeddings/vite.config.ts +++ b/examples/aef-embeddings/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], - base: process.env.VITE_BASE ?? "/deck.gl-raster/examples/embeddings/", + base: process.env.VITE_BASE ?? "/deck.gl-raster/examples/aef-embeddings/", server: { port: 3001, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d75dc8db..c0254e3f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,7 +38,7 @@ importers: specifier: ^5.9.3 version: 5.9.3 - examples/cog-basic: + examples/aef-embeddings: dependencies: '@deck.gl/core': specifier: ^9.2.8 @@ -58,12 +58,24 @@ importers: '@developmentseed/deck.gl-geotiff': specifier: workspace:^ version: link:../../packages/deck.gl-geotiff + '@developmentseed/deck.gl-raster': + specifier: workspace:^ + version: link:../../packages/deck.gl-raster + '@developmentseed/geotiff': + specifier: workspace:^ + version: link:../../packages/geotiff '@luma.gl/core': specifier: ^9.2.6 version: 9.2.6 + '@luma.gl/shadertools': + specifier: ^9.2.6 + version: 9.2.6(@luma.gl/core@9.2.6) maplibre-gl: - specifier: ^5.19.0 + specifier: ^5.17.0 version: 5.19.0 + proj4: + specifier: ^2.20.2 + version: 2.20.3 react: specifier: ^19.2.4 version: 19.2.4 @@ -75,13 +87,13 @@ importers: version: 8.1.0(maplibre-gl@5.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: '@types/react': - specifier: ^19.2.14 + specifier: ^19.2.10 version: 19.2.14 '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': - specifier: ^5.1.4 + specifier: ^5.1.2 version: 5.1.4(vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0)) gh-pages: specifier: ^6.3.0 @@ -90,7 +102,7 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.3.3)(tsx@4.21.0) - examples/embeddings: + examples/cog-basic: dependencies: '@deck.gl/core': specifier: ^9.2.8 @@ -110,24 +122,12 @@ importers: '@developmentseed/deck.gl-geotiff': specifier: workspace:^ version: link:../../packages/deck.gl-geotiff - '@developmentseed/deck.gl-raster': - specifier: workspace:^ - version: link:../../packages/deck.gl-raster - '@developmentseed/geotiff': - specifier: workspace:^ - version: link:../../packages/geotiff '@luma.gl/core': specifier: ^9.2.6 version: 9.2.6 - '@luma.gl/shadertools': - specifier: ^9.2.6 - version: 9.2.6(@luma.gl/core@9.2.6) maplibre-gl: - specifier: ^5.17.0 + specifier: ^5.19.0 version: 5.19.0 - proj4: - specifier: ^2.20.2 - version: 2.20.3 react: specifier: ^19.2.4 version: 19.2.4 @@ -139,13 +139,13 @@ importers: version: 8.1.0(maplibre-gl@5.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) devDependencies: '@types/react': - specifier: ^19.2.10 + specifier: ^19.2.14 version: 19.2.14 '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': - specifier: ^5.1.2 + specifier: ^5.1.4 version: 5.1.4(vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0)) gh-pages: specifier: ^6.3.0 From d17055e889f9d9bb27dc680d42dda4651cb5357c Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 09:29:19 -0700 Subject: [PATCH 11/16] fix: dumb lru cache --- examples/aef-embeddings/src/App.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx index 5b3e3a65..81112086 100644 --- a/examples/aef-embeddings/src/App.tsx +++ b/examples/aef-embeddings/src/App.tsx @@ -29,7 +29,31 @@ function DeckGLOverlay(props: DeckProps) { } type FetchedTile = Awaited>; -const bandCache = new Map(); + +class LRUCache { + private cache = new Map(); + constructor(private maxSize: number) {} + + get(key: K): V | undefined { + const value = this.cache.get(key); + if (value !== undefined) { + this.cache.delete(key); + this.cache.set(key, value); + } + return value; + } + + set(key: K, value: V): void { + this.cache.delete(key); + if (this.cache.size >= this.maxSize) { + const oldest = this.cache.keys().next().value!; + this.cache.delete(oldest); + } + this.cache.set(key, value); + } +} + +const bandCache = new LRUCache(512); function makeTileDataFetcher(bands: [number, number, number]) { return async function getTileData( From c49cfc8ae7212881449c3a59307c0870a7a51b47 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Tue, 3 Mar 2026 09:32:15 -0700 Subject: [PATCH 12/16] fix: midpoint --- examples/aef-embeddings/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx index 81112086..119d6015 100644 --- a/examples/aef-embeddings/src/App.tsx +++ b/examples/aef-embeddings/src/App.tsx @@ -162,8 +162,8 @@ export default function App() {
Date: Tue, 3 Mar 2026 09:33:53 -0700 Subject: [PATCH 13/16] fix: precision --- examples/aef-embeddings/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx index 119d6015..6f3e80fb 100644 --- a/examples/aef-embeddings/src/App.tsx +++ b/examples/aef-embeddings/src/App.tsx @@ -162,8 +162,8 @@ export default function App() {
Date: Wed, 4 Mar 2026 10:10:14 -0700 Subject: [PATCH 14/16] --wip-- [skip ci] --- examples/aef-embeddings/src/App.tsx | 40 +++++++--------- examples/aef-embeddings/vite.config.ts | 1 + pnpm-lock.yaml | 64 ++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/examples/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx index 6f3e80fb..34803b9b 100644 --- a/examples/aef-embeddings/src/App.tsx +++ b/examples/aef-embeddings/src/App.tsx @@ -61,39 +61,31 @@ function makeTileDataFetcher(bands: [number, number, number]) { options: GetTileDataOptions, ): Promise { const { device, x, y, signal } = options; - const tiles = await Promise.all( - bands.map((b) => { - const key = `${x}-${y}-${b}`; - const cached = bandCache.get(key); - if (cached) return cached; - const result = image - .fetchTile(x, y, { signal, boundless: false, band: b }) - .then((tile) => { - bandCache.set(key, tile); - return tile; - }); - return result; - }), - ); - - const { width, height } = tiles[0]!.array; - const pixelCount = width * height; + const tile = await image.fetchTile(x, y, { signal, boundless: false }); + + const pixelCount = tile.array.width * tile.array.height; + console.log(tile.array); const uint8Data = new Uint8Array(pixelCount * 4); for (let i = 0; i < pixelCount; i++) { const outBase = i * 4; for (let c = 0; c < 3; c++) { - const tile = tiles[c]!; - const value = - tile.array.layout === "pixel-interleaved" - ? (tile.array.data[i] as number) - : (tile.array.bands[0]![i] as number); - uint8Data[outBase + c] = value + 128; + if (tile.array.layout === "band-separate") { + throw new Error("band-separate layout is not supported"); + } else { + const value = tile.array.data[i * tile.array.count + bands[c]] as number; + uint8Data[outBase + c] = value + 128; + } } uint8Data[outBase + 3] = 255; } - return { device, data: uint8Data, height, width }; + return { + device, + data: uint8Data, + height: tile.array.height, + width: tile.array.width, + }; }; } diff --git a/examples/aef-embeddings/vite.config.ts b/examples/aef-embeddings/vite.config.ts index 8ff42aec..353d8f26 100644 --- a/examples/aef-embeddings/vite.config.ts +++ b/examples/aef-embeddings/vite.config.ts @@ -4,6 +4,7 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], base: process.env.VITE_BASE ?? "/deck.gl-raster/examples/aef-embeddings/", + worker: { format: "es" }, server: { port: 3001, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d01e3d66..3397d631 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,70 @@ importers: specifier: ^5.9.3 version: 5.9.3 + examples/aef-embeddings: + dependencies: + '@deck.gl/core': + specifier: ^9.2.10 + version: 9.2.10 + '@deck.gl/geo-layers': + specifier: ^9.2.10 + version: 9.2.10(@deck.gl/core@9.2.10)(@deck.gl/extensions@9.2.5(@deck.gl/core@9.2.10)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))))(@deck.gl/layers@9.2.10(@deck.gl/core@9.2.10)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))))(@deck.gl/mesh-layers@9.2.10(@deck.gl/core@9.2.10)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/gltf@9.2.6(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@loaders.gl/core@4.3.4)(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))) + '@deck.gl/layers': + specifier: ^9.2.10 + version: 9.2.10(@deck.gl/core@9.2.10)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6))) + '@deck.gl/mapbox': + specifier: ^9.2.10 + version: 9.2.10(@deck.gl/core@9.2.10)(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@math.gl/web-mercator@4.1.0) + '@deck.gl/mesh-layers': + specifier: ^9.2.10 + version: 9.2.10(@deck.gl/core@9.2.10)(@loaders.gl/core@4.3.4)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/gltf@9.2.6(@luma.gl/constants@9.2.6)(@luma.gl/core@9.2.6)(@luma.gl/engine@9.2.6(@luma.gl/core@9.2.6)(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)))(@luma.gl/shadertools@9.2.6(@luma.gl/core@9.2.6)) + '@developmentseed/deck.gl-geotiff': + specifier: workspace:^ + version: link:../../packages/deck.gl-geotiff + '@developmentseed/deck.gl-raster': + specifier: workspace:^ + version: link:../../packages/deck.gl-raster + '@developmentseed/geotiff': + specifier: workspace:^ + version: link:../../packages/geotiff + '@luma.gl/core': + specifier: ^9.2.6 + version: 9.2.6 + '@luma.gl/shadertools': + specifier: ^9.2.6 + version: 9.2.6(@luma.gl/core@9.2.6) + maplibre-gl: + specifier: ^5.17.0 + version: 5.19.0 + proj4: + specifier: ^2.20.2 + version: 2.20.3 + react: + specifier: ^19.2.4 + version: 19.2.4 + react-dom: + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) + react-map-gl: + specifier: ^8.1.0 + version: 8.1.0(maplibre-gl@5.19.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + devDependencies: + '@types/react': + specifier: ^19.2.10 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.4(vite@7.3.1(@types/node@25.3.3)(tsx@4.21.0)) + gh-pages: + specifier: ^6.3.0 + version: 6.3.0 + vite: + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.3.3)(tsx@4.21.0) + examples/cog-basic: dependencies: '@deck.gl/core': From fb1fc0e058f0800ae5a2c1f7a95430f5efbc361a Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 4 Mar 2026 10:26:03 -0700 Subject: [PATCH 15/16] fix: use new tile stuff --- examples/aef-embeddings/src/App.tsx | 16 ++++++++++------ packages/deck.gl-geotiff/src/cog-layer.ts | 17 +++++++++++++---- packages/geotiff/src/ifd.ts | 6 +++--- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/examples/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx index 34803b9b..0999dfdb 100644 --- a/examples/aef-embeddings/src/App.tsx +++ b/examples/aef-embeddings/src/App.tsx @@ -53,7 +53,7 @@ class LRUCache { } } -const bandCache = new LRUCache(512); +const tileCache = new LRUCache(512); function makeTileDataFetcher(bands: [number, number, number]) { return async function getTileData( @@ -61,20 +61,24 @@ function makeTileDataFetcher(bands: [number, number, number]) { options: GetTileDataOptions, ): Promise { const { device, x, y, signal } = options; - const tile = await image.fetchTile(x, y, { signal, boundless: false }); + const key = `${x}-${y}`; + let tile = tileCache.get(key); + if (!tile) { + tile = await image.fetchTile(x, y, { signal, boundless: false }); + tileCache.set(key, tile); + } const pixelCount = tile.array.width * tile.array.height; - console.log(tile.array); const uint8Data = new Uint8Array(pixelCount * 4); for (let i = 0; i < pixelCount; i++) { const outBase = i * 4; for (let c = 0; c < 3; c++) { if (tile.array.layout === "band-separate") { - throw new Error("band-separate layout is not supported"); - } else { - const value = tile.array.data[i * tile.array.count + bands[c]] as number; + const value = tile.array.bands[bands[c]][i] as number; uint8Data[outBase + c] = value + 128; + } else { + throw new Error("pixel-interleaved layout is not supported"); } } uint8Data[outBase + 3] = 255; diff --git a/packages/deck.gl-geotiff/src/cog-layer.ts b/packages/deck.gl-geotiff/src/cog-layer.ts index 4197af13..5f5824c3 100644 --- a/packages/deck.gl-geotiff/src/cog-layer.ts +++ b/packages/deck.gl-geotiff/src/cog-layer.ts @@ -73,8 +73,9 @@ type GetTileDataResult = { inverseTransform: ReprojectionFns["inverseTransform"]; }; -export interface COGLayerProps - extends CompositeLayerProps { +export interface COGLayerProps< + DataT extends MinimalDataT = DefaultDataT, +> extends CompositeLayerProps { /** * Cloud-optimized GeoTIFF input. * @@ -250,8 +251,16 @@ export class COGLayer< }); } - const { getTileData: defaultGetTileData, renderTile: defaultRenderTile } = - inferRenderPipeline(geotiff, this.context.device); + let defaultGetTileData: + | COGLayerProps["getTileData"] + | undefined; + let defaultRenderTile: + | COGLayerProps["renderTile"] + | undefined; + if (!this.props.getTileData || !this.props.renderTile) { + ({ getTileData: defaultGetTileData, renderTile: defaultRenderTile } = + inferRenderPipeline(geotiff, this.context.device)); + } this.setState({ geotiff, diff --git a/packages/geotiff/src/ifd.ts b/packages/geotiff/src/ifd.ts index c4839243..f162d3ab 100644 --- a/packages/geotiff/src/ifd.ts +++ b/packages/geotiff/src/ifd.ts @@ -28,8 +28,6 @@ export async function prefetchTags(image: TiffImage): Promise { throw new Error("Compression tag should always exist."); } - const nodata = image.noData; - const [ bitsPerSample, colorMap, @@ -43,6 +41,7 @@ export async function prefetchTags(image: TiffImage): Promise { samplesPerPixel, tileByteCounts, tileOffsets, + nodata, ] = await Promise.all([ image.fetch(TiffTag.BitsPerSample), image.fetch(TiffTag.ColorMap), @@ -59,6 +58,7 @@ export async function prefetchTags(image: TiffImage): Promise { // results in many redundant requests. image.fetch(TiffTag.TileByteCounts), image.fetch(TiffTag.TileOffsets), + image.fetch(TiffTag.GdalNoData), ]); const missingTag: (tagName: string) => never = (tagName: string) => { @@ -88,7 +88,7 @@ export async function prefetchTags(image: TiffImage): Promise { modelTiepoint, modelPixelScale, modelTransformation, - nodata, + nodata: Number(nodata), photometric, planarConfiguration, predictor: (predictor as Predictor) ?? Predictor.None, From f1741a97ec60400012c41001f135574741b1db49 Mon Sep 17 00:00:00 2001 From: Pete Gadomski Date: Wed, 4 Mar 2026 15:53:50 -0700 Subject: [PATCH 16/16] fix: remove unused stuff --- packages/geotiff/src/ifd.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/geotiff/src/ifd.ts b/packages/geotiff/src/ifd.ts index e9b15087..960a74cd 100644 --- a/packages/geotiff/src/ifd.ts +++ b/packages/geotiff/src/ifd.ts @@ -44,7 +44,6 @@ export async function prefetchTags(image: TiffImage): Promise { samplesPerPixel, tileByteCounts, tileOffsets, - nodata, ] = await Promise.all([ image.fetch(TiffTag.BitsPerSample), image.fetch(TiffTag.ColorMap), @@ -63,7 +62,6 @@ export async function prefetchTags(image: TiffImage): Promise { // results in many redundant requests. image.fetch(TiffTag.TileByteCounts), image.fetch(TiffTag.TileOffsets), - image.fetch(TiffTag.GdalNoData), ]); const missingTag: (tagName: string) => never = (tagName: string) => {