diff --git a/examples/aef-embeddings/README.md b/examples/aef-embeddings/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/aef-embeddings/index.html b/examples/aef-embeddings/index.html new file mode 100644 index 00000000..87c554fc --- /dev/null +++ b/examples/aef-embeddings/index.html @@ -0,0 +1,22 @@ + + + + + + Embeddings Example + + + +
+ + + diff --git a/examples/aef-embeddings/package.json b/examples/aef-embeddings/package.json new file mode 100644 index 00000000..21ab1594 --- /dev/null +++ b/examples/aef-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/aef-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/aef-embeddings/src/App.tsx b/examples/aef-embeddings/src/App.tsx new file mode 100644 index 00000000..0999dfdb --- /dev/null +++ b/examples/aef-embeddings/src/App.tsx @@ -0,0 +1,207 @@ +import type { DeckProps } from "@deck.gl/core"; +import { MapboxOverlay } from "@deck.gl/mapbox"; +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"; +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 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; + data: Uint8Array; + height: number; + width: number; +}; + +function DeckGLOverlay(props: DeckProps) { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + return null; +} + +type FetchedTile = Awaited>; + +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 tileCache = new LRUCache(512); + +function makeTileDataFetcher(bands: [number, number, number]) { + return async function getTileData( + image: GeoTIFF | Overview, + options: GetTileDataOptions, + ): Promise { + const { device, x, y, signal } = options; + 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; + 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") { + 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; + } + + return { + device, + data: uint8Data, + height: tile.array.height, + width: tile.array.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 [cogUrl, setCogUrl] = useState(DEFAULT_COG_URL); + 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-${cogUrl}-${r}-${g}-${b}`, + geotiff: cogUrl, + getTileData: getTileData(), + renderTile, + }); + + return ( +
+ + + +
+ 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, + }} + /> + + + +
+
+ ); +} diff --git a/examples/aef-embeddings/src/main.tsx b/examples/aef-embeddings/src/main.tsx new file mode 100644 index 00000000..f8fc6f51 --- /dev/null +++ b/examples/aef-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/aef-embeddings/tsconfig.json b/examples/aef-embeddings/tsconfig.json new file mode 100644 index 00000000..f0a23505 --- /dev/null +++ b/examples/aef-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/aef-embeddings/vite.config.ts b/examples/aef-embeddings/vite.config.ts new file mode 100644 index 00000000..353d8f26 --- /dev/null +++ b/examples/aef-embeddings/vite.config.ts @@ -0,0 +1,11 @@ +import react from "@vitejs/plugin-react"; +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 8b74a458..7e26ce3b 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':