diff --git a/package-lock.json b/package-lock.json index 7a89790..2b94ad5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,12 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-map-gl": "^8.0.2", + "react-router": "^7.5.3", "zarrita": "^0.5.1" }, "devDependencies": { "@eslint/js": "^9.21.0", + "@netlify/vite-plugin-react-router": "^1.0.1", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", @@ -2097,6 +2099,29 @@ "@math.gl/core": "4.1.0" } }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "dev": true + }, + "node_modules/@netlify/vite-plugin-react-router": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@netlify/vite-plugin-react-router/-/vite-plugin-react-router-1.0.1.tgz", + "integrity": "sha512-JE32RJ6PamX6Yf2yyys7NLaKK4ppcDwuL6mk7NDZ7rt4EWUm+5bz/YjMcLz9uXyo1jDPlWjaUcqRC2MUZKt2DA==", + "dev": true, + "dependencies": { + "@react-router/node": "^7.0.1", + "isbot": "^5.0.0", + "react-router": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "vite": ">=5.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2179,6 +2204,30 @@ "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.0.tgz", "integrity": "sha512-EI413MkWKBDVNIfLdqbeNSJTs7ToBz/KVGkwi3D+dQrSIkRI2IYbWGAU3xX+D6+CI4ls8ehxMhNpUVMaZggDvQ==" }, + "node_modules/@react-router/node": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.6.0.tgz", + "integrity": "sha512-agjDPUzisLdGJ7Q2lx/Z3OfdS2t1k6qv/nTvA45iahGsQJCMDvMqVoIi7iIULKQJwrn4HWjM9jqEp75+WsMOXg==", + "dev": true, + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0", + "source-map-support": "^0.5.21", + "stream-slice": "^0.1.2", + "undici": "^6.19.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.6.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.9", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", @@ -4453,6 +4502,12 @@ "node": ">=0.10.0" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/bytewise": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", @@ -4669,6 +4724,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, "node_modules/core-assert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/core-assert/-/core-assert-0.2.1.tgz", @@ -5783,6 +5846,15 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "node_modules/isbot": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -6672,6 +6744,27 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", + "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -6831,6 +6924,11 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -6937,6 +7035,25 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/splaytree-ts": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", @@ -6993,6 +7110,12 @@ "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", "dev": true }, + "node_modules/stream-slice": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", + "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7124,6 +7247,48 @@ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", @@ -7257,6 +7422,15 @@ "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "dev": true, + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", @@ -7342,14 +7516,17 @@ "integrity": "sha512-AMqwWZaknLM77G+VPYNZLEruMGWGzyigPK3/Whg99B3S6vGHuqsyl5ZrOv1UUF3paGK1U6PM0cnayioaryg/fA==" }, "node_modules/vite": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", - "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, "dependencies": { "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -7434,6 +7611,32 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", diff --git a/package.json b/package.json index 9aedc3c..db5e459 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,12 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-map-gl": "^8.0.2", + "react-router": "^7.5.3", "zarrita": "^0.5.1" }, "devDependencies": { "@eslint/js": "^9.21.0", + "@netlify/vite-plugin-react-router": "^1.0.1", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..50a4633 --- /dev/null +++ b/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index e7becdc..fc7bcdd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -158,11 +158,11 @@ function App() { - + onChange={setSelectedColormap} /> diff --git a/src/Titiler.tsx b/src/Titiler.tsx new file mode 100644 index 0000000..ea892d6 --- /dev/null +++ b/src/Titiler.tsx @@ -0,0 +1,177 @@ +import { useState } from "react"; +import { Map, NavigationControl, useControl } from "react-map-gl/maplibre"; +import { TileLayer } from "@deck.gl/geo-layers"; +import type { _TileLoadProps } from "@deck.gl/geo-layers"; + +import { MapboxOverlay as DeckOverlay } from "@deck.gl/mapbox"; +// @ts-expect-error npy is not typed yet +import { parseNpy } from "./utils/npy"; +import NumericDataLayer from "@/layers/NumericDataLayer"; +import type { NumericDataPickingInfo } from "@/layers/NumericDataLayer/types"; +import Panel from "@/components/Panel"; +import Dropdown from "@/components/ui/Dropdown"; +import type { Option } from "@/components/ui/Dropdown"; +import RangeSlider from "@/components/ui/RangeSlider"; +import CheckBox from "@/components/ui/Checkbox"; + +import "maplibre-gl/dist/maplibre-gl.css"; +import "./App.css"; + +const INITIAL_VIEW_STATE = { + latitude: 1.3567, + longitude: 172.933, + zoom: 15, + maxZoom: 20, +}; + +const MAP_STYLE = + "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"; + +//@ts-expect-error ignoring for now +function DeckGLOverlay(props) { + const overlay = useControl(() => new DeckOverlay(props)); + overlay.setProps(props); + return null; +} + +function App() { + const [selectedColormap, setSelectedColormap] = useState("viridis"); + const [minMax, setMinMax] = useState<{ min: number; max: number }>({ + min: 3000, + max: 18000, + }); + const [bandRange, setBandRange] = useState[]>([ + { value: 0, label: "0" }, + ]); + const [selectedBand, setSelectedBand] = useState(0); + + const [showTooltip, setShowTooltip] = useState(true); + + async function getTileData({ index, signal }: _TileLoadProps) { + if (signal?.aborted) { + console.error("Signal aborted: ", signal); + return null; + } + const { z, x, y } = index; + // using the same output from https://openlayers.org/en/latest/examples/numpytile.html + const url = `https://titiler.xyz/cog/tiles/WebMercatorQuad/${z}/${x}/${y}@1x?format=npy&url=https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif`; + const resp = await fetch(url); + if (!resp.ok) { + return null; + } + + const { dtype, data, header } = parseNpy(await resp.arrayBuffer()); + + if (bandRange !== header.shape[0]) + setBandRange( + new Array(header.shape[0]) + .fill(0) + .map((_, idx) => ({ label: idx.toString(), value: idx })) + ); + return { dtype, data }; + } + + const layers = [ + new TileLayer({ + id: "TileLayer", + // data: 'https://c.tile.openstreetmap.org/{z}/{x}/{y}.png', + + /* props from TileLayer class */ + // TilesetClass: null, + // debounceTime: 0, + // extent: null, + getTileData, + // maxCacheByteSize: null, + // maxCacheSize: null, + // maxRequests: 6, + maxZoom: 16, + minZoom: 0, + // onTileError: null, + // onTileLoad: null, + // onTileUnload: null, + // onViewportLoad: null, + // refinementStrategy: 'best-available', + // Any better way to do this? + updateTriggers: { + renderSubLayers: [selectedColormap, minMax, selectedBand], + }, + renderSubLayers: (props) => { + const { data } = props.data; + const { boundingBox } = props.tile; + // Cast into float32 (?? integer texture) + const slicedData = new Float32Array( + data.slice(256 * 256 * selectedBand, 256 * 256 * (selectedBand + 1)) + ); + return new NumericDataLayer(props, { + data: undefined, + colormap_image: `/colormaps/${selectedColormap}.png`, + min: minMax.min, + max: minMax.max, + tileSize: 256, + imageData: slicedData, + bounds: [ + boundingBox[0][0], + boundingBox[0][1], + boundingBox[1][0], + boundingBox[1][1], + ], + pickable: true, + }); + }, + tileSize: 256, + // zRange: null, + // zoomOffset: 0, + + /* props inherited from Layer class */ + + // autoHighlight: false, + // coordinateOrigin: [0, 0, 0], + // coordinateSystem: COORDINATE_SYSTEM.LNGLAT, + // highlightColor: [0, 0, 128, 128], + // modelMatrix: null, + // opacity: 1, + + // visible: true, + // wrapLongitude: false, + }), + ]; + + const deckProps = { + layers, + getTooltip: (info: NumericDataPickingInfo) => { + return showTooltip ? info.dataValue && `${info.dataValue}` : null; + }, + }; + + return ( + <> + + 1 + + + + + onChange={setSelectedColormap} /> + + + label="Select band " + options={bandRange} + defaultValue={selectedBand} + onChange={setSelectedBand} + /> + + + + ); +} + +export default App; diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index 14f0aa0..b5ef5ee 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -3,39 +3,57 @@ import { Portal, Select, createListCollection } from "@chakra-ui/react"; import { baseZIndex } from "../Panel"; -type Option = { - value: string; +export type Option = { + value: T; label: string; }; -interface DropdownProps { - onChange: (colormapName: string) => void; - options?: Option[]; - defaultValue?: string; +interface DropdownProps { + label?: string; + onChange: (value: T) => void; + options?: Option[]; + defaultValue?: T; } -const colormapOptions = [ +const colormapOptions: Option[] = [ { label: "VIRIDIS", value: "viridis" }, { label: "CIVIDIS", value: "cividis" }, ]; -const Dropdown = ({ +const Dropdown = ({ onChange, - options = colormapOptions, - defaultValue = "viridis", -}: DropdownProps) => { - const [selected, setSelected] = useState(defaultValue); + label = "Select colormap", + options = colormapOptions as Option[], + defaultValue, +}: DropdownProps) => { + // Convert the value to string for Chakra UI Select + const defaultStringValue = + defaultValue?.toString() ?? options[0]?.value?.toString() ?? ""; + const [selected, setSelected] = useState(defaultStringValue); + + // Create a map to convert string back to original type + const valueMap = new Map(); + options.forEach((option) => { + valueMap.set(option.value.toString(), option.value); + }); const colorMaps = createListCollection({ - items: options, + items: options.map((option) => ({ + label: option.label, + value: option.value.toString(), // Convert to string for Chakra UI + })), }); - const handleChange = (event: Select.ValueChangeDetails