Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
22 changes: 22 additions & 0 deletions examples/aef-embeddings/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Embeddings Example</title>
<style>
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#root {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions examples/aef-embeddings/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
207 changes: 207 additions & 0 deletions examples/aef-embeddings/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<MapboxOverlay>(() => new MapboxOverlay(props));
overlay.setProps(props);
return null;
}

type FetchedTile = Awaited<ReturnType<GeoTIFF["fetchTile"]>>;

class LRUCache<K, V> {
private cache = new Map<K, V>();
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<string, FetchedTile>(512);

function makeTileDataFetcher(bands: [number, number, number]) {
return async function getTileData(
image: GeoTIFF | Overview,
options: GetTileDataOptions,
): Promise<TileData> {
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 (
<label style={{ display: "flex", alignItems: "center", gap: 6 }}>
<span style={{ width: 14, fontWeight: 600 }}>{label}</span>
<input
type="number"
min={0}
max={NUM_BANDS - 1}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
style={{ width: 52 }}
/>
<input
type="range"
min={0}
max={NUM_BANDS - 1}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
/>
</label>
);
}

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<TileData>({
id: `embeddings-layer-${cogUrl}-${r}-${g}-${b}`,
geotiff: cogUrl,
getTileData: getTileData(),
renderTile,
});

return (
<div style={{ width: "100%", height: "100%", position: "relative" }}>
<MaplibreMap
initialViewState={{
longitude: -105.484815,
latitude: 40.330301,
zoom: 10,
}}
style={{ width: "100%", height: "100%" }}
mapStyle="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
>
<DeckGLOverlay layers={[layer]} />
</MaplibreMap>
<div
style={{
position: "absolute",
top: 10,
left: 10,
zIndex: 10,
background: "rgba(0,0,0,0.8)",
color: "#fff",
padding: "10px 14px",
borderRadius: 6,
fontSize: 13,
display: "flex",
flexDirection: "column",
gap: 6,
}}
>
<input
type="text"
value={cogUrl}
onChange={(e) => 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,
}}
/>
<BandSelector label="R" value={r} onChange={setR} />
<BandSelector label="G" value={g} onChange={setG} />
<BandSelector label="B" value={b} onChange={setB} />
</div>
</div>
);
}
9 changes: 9 additions & 0 deletions examples/aef-embeddings/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);
24 changes: 24 additions & 0 deletions examples/aef-embeddings/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
11 changes: 11 additions & 0 deletions examples/aef-embeddings/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
64 changes: 64 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading