From 8833e4de82476dd85530d01dfab1ead44b188815 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 16 May 2025 15:15:43 -0400 Subject: [PATCH 1/8] Add zoom --- src/App.tsx | 7 ++++--- src/zarr/index.ts | 47 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 96c474c..e7becdc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,7 +22,7 @@ const INITIAL_VIEW_STATE = { latitude: 51.47, longitude: 0.45, zoom: 0, - maxZoom: 1, + maxZoom: 20, }; const MAP_STYLE = @@ -88,8 +88,8 @@ function App() { // maxCacheByteSize: null, // maxCacheSize: null, // maxRequests: 6, - // maxZoom: 19, - // minZoom: 0, + maxZoom: zarrReader.maxZoom, + minZoom: zarrReader.minZoom, // onTileError: null, // onTileLoad: null, // onTileUnload: null, @@ -162,6 +162,7 @@ function App() { diff --git a/src/zarr/index.ts b/src/zarr/index.ts index c096b79..3189f73 100644 --- a/src/zarr/index.ts +++ b/src/zarr/index.ts @@ -7,16 +7,22 @@ import type { NumberDataType, } from "zarrita"; import { findMinMax } from "./utils"; -interface ZarrReaderProps { +type ZarrReaderProps = { zarrUrl: string; varName: string; -} +}; -interface TileIndex { +type TileIndex = { x: number; y: number; z: number; -} +}; + +type Multiscale = { + tile_matrix_set: "WebMercatorQuad"; + resampling_method: string; + tile_matrix_limits: Record; +}; export default class ZarrReader { private root!: Location; @@ -28,6 +34,7 @@ export default class ZarrReader { private _tileSize: number = 256; // @TODO: hard coding for now private _t: number = 0; + private _zooms!: { min: number; max: number }; get scale() { return this._scale; @@ -41,6 +48,12 @@ export default class ZarrReader { get metadata() { return this._metadata; } + get minZoom() { + return this._zooms.min; + } + get maxZoom() { + return this._zooms.max; + } private constructor() {} @@ -74,19 +87,37 @@ export default class ZarrReader { min: minMax.min, }; } + + const zarrMetadata = await zarr.open.v3(this.root.resolve(`${varName}`), { + kind: "array", + }); + const multiscale = zarrMetadata.attrs.multiscales as Multiscale; + + if (multiscale?.tile_matrix_limits) { + this._zooms = { + min: Math.min( + ...Object.keys(multiscale.tile_matrix_limits).map((e) => Number(e)) + ), + max: Math.max( + ...Object.keys(multiscale.tile_matrix_limits).map((e) => Number(e)) + ), + }; + } } async getTileData({ x, y, - z, timestamp, }: TileIndex & { timestamp: number }): Promise< TypedArray | undefined > { - const arr = await zarr.open.v3(this.root.resolve(`${z}/${this._varName}`), { - kind: "array", - }); + const arr = await zarr.open.v3( + this.root.resolve(`${this._t}/${this._varName}`), + { + kind: "array", + } + ); if (arr.is("number")) { const { data } = await arr.getChunk([timestamp, y, x]); From 79a3c4fda1fa788e662c49e65b5667eaeb29ee57 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 6 May 2025 16:28:28 -0400 Subject: [PATCH 2/8] Datetime selection #2 --- src/App.tsx | 1 - src/zarr/index.ts | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e7becdc..384ff26 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -162,7 +162,6 @@ function App() { diff --git a/src/zarr/index.ts b/src/zarr/index.ts index 3189f73..27d8c32 100644 --- a/src/zarr/index.ts +++ b/src/zarr/index.ts @@ -112,17 +112,14 @@ export default class ZarrReader { }: TileIndex & { timestamp: number }): Promise< TypedArray | undefined > { - const arr = await zarr.open.v3( - this.root.resolve(`${this._t}/${this._varName}`), - { - kind: "array", - } - ); + const arr = await zarr.open.v3(this.root.resolve(`${z}/${this._varName}`), { + kind: "array", + }); if (arr.is("number")) { const { data } = await arr.getChunk([timestamp, y, x]); // @TODO : remove once the data has actual timestamps - if (timestamp == 2) { + if (timestamp == 1) { return new Float32Array(this.tileSize * this.tileSize); } From a3d01afa68e81f5397c54ca54c9aac9cb165d905 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 16 May 2025 15:21:43 -0400 Subject: [PATCH 3/8] Animation throughout timestamps --- package-lock.json | 120 ++++++++- package.json | 1 + src/Animation.tsx | 239 ++++++++++++++++++ src/components/ui/Slider.tsx | 1 + .../NumericDataAnimationLayer/index.tsx | 109 ++++++++ .../NumericDataAnimationLayer/types.d.ts | 13 + .../NumericDataAnimationPaintLayer/index.tsx | 100 ++++++++ .../NumericDataAnimationPaintLayer/types.d.ts | 9 + src/main.tsx | 10 +- src/zarr/index.ts | 4 +- 10 files changed, 600 insertions(+), 6 deletions(-) create mode 100644 src/Animation.tsx create mode 100644 src/layers/NumericDataAnimationLayer/index.tsx create mode 100644 src/layers/NumericDataAnimationLayer/types.d.ts create mode 100644 src/layers/NumericDataAnimationPaintLayer/index.tsx create mode 100644 src/layers/NumericDataAnimationPaintLayer/types.d.ts diff --git a/package-lock.json b/package-lock.json index 7a89790..d195e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "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": { @@ -4669,6 +4670,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", @@ -6672,6 +6681,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", + "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.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 +6862,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", @@ -7124,6 +7160,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", @@ -7185,6 +7263,11 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7342,14 +7425,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 +7520,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..45c8f16 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "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": { diff --git a/src/Animation.tsx b/src/Animation.tsx new file mode 100644 index 0000000..b94a72b --- /dev/null +++ b/src/Animation.tsx @@ -0,0 +1,239 @@ +import { useState, useEffect } from "react"; +import { + Map as GLMap, + 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"; + +import ZarrReader from "./zarr"; +import NumericDataAnimationLayer from "@/layers/NumericDataAnimationLayer"; +import type { NumericDataPickingInfo } from "@/layers/NumericDataLayer/types"; +import Panel from "@/components/Panel"; +import Description from "@/components/Description"; +import Dropdown from "@/components/ui/Dropdown"; +import RangeSlider from "@/components/ui/RangeSlider"; +import SingleSlider from "@/components/ui/Slider"; +import CheckBox from "@/components/ui/Checkbox"; + +import "maplibre-gl/dist/maplibre-gl.css"; +import "./App.css"; + +const INITIAL_VIEW_STATE = { + latitude: 51.47, + longitude: 0.45, + zoom: 0, + maxZoom: 1, +}; + +const MAP_STYLE = + "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"; + +const BASE_URL = import.meta.env.VITE_ZARR_BASE_URL ?? window.location.origin; + +const ZARR_STORE_NAME = + "20020601090000-JPL-L4_GHRSST-SSTfnd-MUR-GLOB-v02.0-fv04.1_multiscales.zarr"; + +const VAR_NAME = "analysed_sst"; + +const zarrReader = await ZarrReader.initialize({ + zarrUrl: `${BASE_URL}/${ZARR_STORE_NAME}`, + varName: VAR_NAME, +}); + +const timestampUnit = 1; +const maxTimestamp = 2; + +const quickCache = new Map(); + +//@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 }>( + zarrReader.scale + ); + const [timestamp, setTimestamp] = useState(0.0); + const timestampStart = Math.floor(timestamp); + const timestampEnd = Math.min( + Math.floor(timestamp + timestampUnit), + maxTimestamp + ); + const [showTooltip, setShowTooltip] = useState(false); + + async function fetchOneTimeStamp({ timestamp, index }) { + const { x, y, z } = index; + const keyName = `tile${timestamp}${x}${y}${z}`; + const chunkData = await zarrReader.getTileData({ + ...index, + timestamp, + }); + quickCache.set(keyName, chunkData); + return chunkData; + } + + async function getTileData({ index, signal }: _TileLoadProps) { + if (signal?.aborted) { + console.error("Signal aborted: ", signal); + return null; + } + const scale = zarrReader.scale; + + const { min, max } = scale; + const { x, y, z } = index; + let chunkDataStart; + let chunkDataEnd; + const timestampKeyStart = `tile${timestampStart}${x}${y}${z}`; + const timestampKeyEnd = `tile${timestampEnd}${x}${y}${z}`; + // Make it synchronous when there the value is cached + if (quickCache.get(timestampKeyStart) && quickCache.get(timestampKeyEnd)) { + return { + imageDataStart: quickCache.get(timestampKeyStart), + imageDataEnd: quickCache.get(timestampKeyEnd), + min, + max, + }; + } + + const oneMoreStamp = Math.min(timestampEnd + 1, maxTimestamp); + await fetchOneTimeStamp({ timestamp: oneMoreStamp, index }); + + if (!quickCache.get(timestampKeyStart)) { + chunkDataStart = await fetchOneTimeStamp({ + index, + timestamp: timestampStart, + }); + } else { + chunkDataStart = quickCache.get(timestampKeyStart); + } + + if (!quickCache.get(timestampKeyEnd)) { + chunkDataEnd = await fetchOneTimeStamp({ + index, + timestamp: timestampEnd, + }); + } else { + chunkDataEnd = quickCache.get(timestampKeyEnd); + } + + if (chunkDataStart && chunkDataEnd) { + return { + imageDataStart: chunkDataStart, + imageDataEnd: chunkDataEnd, + min, + max, + }; + } else { + throw Error("No tile data available"); + } + } + + 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: 19, + // minZoom: 0, + // onTileError: null, + // onTileLoad: null, + // onTileUnload: null, + // onViewportLoad: null, + // refinementStrategy: 'best-available', + updateTriggers: { + getTileData: [timestampStart, timestampEnd], + renderSubLayers: [selectedColormap, minMax, timestamp], + }, + renderSubLayers: (props) => { + const { imageDataStart, imageDataEnd } = props.data; + const { boundingBox } = props.tile; + return new NumericDataAnimationLayer(props, { + data: undefined, + colormap_image: `/colormaps/${selectedColormap}.png`, + min: minMax.min, + max: minMax.max, + imageDataStart, + imageDataEnd, + timestamp: timestamp - timestampStart, + tileSize: zarrReader.tileSize, + bounds: [ + boundingBox[0][0], + boundingBox[0][1], + boundingBox[1][0], + boundingBox[1][1], + ], + pickable: true, + }); + }, + tileSize: zarrReader.tileSize, + // 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 ( + <> + + + + + + + + + + + + + ); +} + +export default App; diff --git a/src/components/ui/Slider.tsx b/src/components/ui/Slider.tsx index 7455887..04b7970 100644 --- a/src/components/ui/Slider.tsx +++ b/src/components/ui/Slider.tsx @@ -32,6 +32,7 @@ export default function SingleSlider({ + ); diff --git a/src/layers/NumericDataAnimationLayer/index.tsx b/src/layers/NumericDataAnimationLayer/index.tsx new file mode 100644 index 0000000..177ade4 --- /dev/null +++ b/src/layers/NumericDataAnimationLayer/index.tsx @@ -0,0 +1,109 @@ +import { + CompositeLayer, + Layer, + LayersList, + LayerContext, + GetPickingInfoParams, +} from "deck.gl"; +import type { CompositeLayerProps, UpdateParameters } from "deck.gl"; +import type { BitmapLayerPickingInfo } from "@deck.gl/layers"; +import type { Texture } from "@luma.gl/core"; + +import { NumericDataAnimationPaintLayer } from "../NumericDataAnimationPaintLayer"; +import type { NumericDataAnimationLayerProps } from "./types"; + +const textureDefaultOption = { + format: "r32float" as const, + dimension: "2d" as const, +}; + +const textureSamplerDefaultOption = { + minFilter: "linear" as const, + magFilter: "linear" as const, + mipmapFilter: "linear" as const, + addressModeU: "clamp-to-edge" as const, + addressModeV: "clamp-to-edge" as const, +}; + +export default class NumericDataAnimationLayer extends CompositeLayer { + static layerName: string = "numeric-data-animation-layer"; + + // initializeState(context: LayerContext): void { + // const { tileSize, textureParameters } = this.props; + // const dataTextureStart = context.device.createTexture({ + // data: this.props.imageDataStart, + // width: tileSize, + // height: tileSize, + // ...textureDefaultOption, + // sampler: { + // ...textureSamplerDefaultOption, + // ...textureParameters, + // }, + // }); + // const dataTextureEnd = context.device.createTexture({ + // data: this.props.imageDataEnd, + // width: tileSize, + // height: tileSize, + // ...textureDefaultOption, + // sampler: { + // ...textureSamplerDefaultOption, + // ...textureParameters, + // }, + // }); + // this.setState({ + // dataTextureStart, + // dataTextureEnd, + // }); + // } + updateState( + params: UpdateParameters< + Layer> + > + ): void { + const { props, oldProps, context } = params; + const { imageDataStart, imageDataEnd, timestamp } = props; + const { + imageDataStart: oldImageDataStart, + imageDataEnd: oldImageDataEnd, + timestamp: oldTimestamp, + } = oldProps; + if ( + imageDataStart !== oldImageDataStart && + imageDataEnd !== oldImageDataEnd + ) { + const { tileSize, textureParameters } = props; + const dataTextureStart = context.device.createTexture({ + data: this.props.imageDataStart, + width: tileSize, + height: tileSize, + ...textureDefaultOption, + sampler: { + ...textureSamplerDefaultOption, + ...textureParameters, + }, + }); + const dataTextureEnd = context.device.createTexture({ + data: this.props.imageDataEnd, + width: tileSize, + height: tileSize, + ...textureDefaultOption, + sampler: { + ...textureSamplerDefaultOption, + ...textureParameters, + }, + }); + this.setState({ + dataTextureStart, + dataTextureEnd, + }); + } + } + + renderLayers(): Layer | null | LayersList { + return new NumericDataAnimationPaintLayer(this.props, { + id: `${this.props.id}-data`, + image: this.state.dataTextureStart as Texture, + imageEnd: this.state.dataTextureEnd as Texture, + }); + } +} diff --git a/src/layers/NumericDataAnimationLayer/types.d.ts b/src/layers/NumericDataAnimationLayer/types.d.ts new file mode 100644 index 0000000..efd8795 --- /dev/null +++ b/src/layers/NumericDataAnimationLayer/types.d.ts @@ -0,0 +1,13 @@ +import type { NumericDataPaintLayerProps } from "../NumericDataPaintLayer/types"; +import type { BitmapLayerPickingInfo } from "@deck.gl/layers"; +import type { TypedArray, NumberDataType } from "zarrita"; + +export interface NumericDataAnimationLayerProps + extends NumericDataPaintLayerProps { + imageDataStart: TypedArray; + imageDataEnd: TypedArray; +} + +export interface NumericDataPickingInfo extends BitmapLayerPickingInfo { + dataValue: NumberDataType | null; +} diff --git a/src/layers/NumericDataAnimationPaintLayer/index.tsx b/src/layers/NumericDataAnimationPaintLayer/index.tsx new file mode 100644 index 0000000..71ae717 --- /dev/null +++ b/src/layers/NumericDataAnimationPaintLayer/index.tsx @@ -0,0 +1,100 @@ +import { BitmapLayer } from "deck.gl"; + +import type { Texture } from "@luma.gl/core"; +import type { ShaderModule } from "@luma.gl/shadertools"; + +import type { NumericDataPaintLayerProps } from "./types"; + +const uniformBlock = `\ + uniform ndUniforms { + float min; + float max; + float step; + } nd; +`; + +export type NDProps = { + min: number; + max: number; + step: number; + colormap_texture: Texture; + image_end: Texture; +}; + +const numericDataAnimationUniforms = { + name: "nd", + vs: uniformBlock, + fs: uniformBlock, + // @?: not float data? + uniformTypes: { + min: "f32", + max: "f32", + step: "f32", + }, +} as const satisfies ShaderModule; + +const defaultProps = { + ...BitmapLayer.defaultProps, + min: 0, + max: 0, + tileSize: 256, + timestamp: 0.0, + imageData: [], + colormap_image: { + type: "image", + value: null, + async: true, + }, +}; + +export class NumericDataAnimationPaintLayer extends BitmapLayer { + static layerName = "numeric-paint-animation-layer"; + static defaultProps = defaultProps; + + getShaders() { + return { + ...super.getShaders(), + inject: { + "fs:#decl": ` + uniform sampler2D colormap_texture; // texture is not included in ubo + uniform sampler2D image_end; + `, + "fs:DECKGL_FILTER_COLOR": ` + + float start_value = color.r; + vec4 end_image = texture(image_end, geometry.uv); + float end_value = end_image.r; + float value = mix(start_value, end_value, nd.step); + if (isnan(value)) { + discard; + } else { + float normalized = (value - nd.min)/(nd.max - nd.min); + vec4 color_val = texture(colormap_texture, vec2(normalized, 0.)); + color = color_val; + } + `, + }, + modules: [...super.getShaders().modules, numericDataAnimationUniforms], + }; + } + + // @ts-expect-error no opts type available + draw(opts) { + const { colormap_image, imageEnd, timestamp, min, max } = this.props; + + const sModels = super.getModels(); + if (colormap_image) + for (const m of sModels) { + m.shaderInputs.setProps({ + nd: { + colormap_texture: colormap_image, + image_end: imageEnd, + step: timestamp, + min, + max, + }, + }); + } + super.draw({ ...opts }); + } +} diff --git a/src/layers/NumericDataAnimationPaintLayer/types.d.ts b/src/layers/NumericDataAnimationPaintLayer/types.d.ts new file mode 100644 index 0000000..b138287 --- /dev/null +++ b/src/layers/NumericDataAnimationPaintLayer/types.d.ts @@ -0,0 +1,9 @@ +import type { BitmapLayerProps } from "deck.gl"; + +export interface NumericDataPaintLayerProps extends BitmapLayerProps { + colormap_image: string | Texture; + min: number; + max: number; + timestamp: number; + tileSize: number; +} diff --git a/src/main.tsx b/src/main.tsx index 8f027f3..703441e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,19 @@ import { createRoot } from "react-dom/client"; import { Provider } from "@/components/ui/Provider"; import { defaultSystem } from "@chakra-ui/react"; +import { BrowserRouter, Routes, Route } from "react-router"; + import "./index.css"; import App from "./App.tsx"; +import Animation from "./Animation.tsx"; createRoot(document.getElementById("root")!).render( - + + + } /> + } /> + + ); diff --git a/src/zarr/index.ts b/src/zarr/index.ts index 27d8c32..969a031 100644 --- a/src/zarr/index.ts +++ b/src/zarr/index.ts @@ -120,7 +120,9 @@ export default class ZarrReader { const { data } = await arr.getChunk([timestamp, y, x]); // @TODO : remove once the data has actual timestamps if (timestamp == 1) { - return new Float32Array(this.tileSize * this.tileSize); + const tempArray = new Float32Array(this.tileSize * this.tileSize * 2); + tempArray.fill(this.scale.min); + return tempArray; } return data; From 7df1de4870daad07c64db1d5fd2594367c5c00c7 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 14 May 2025 12:07:44 -0400 Subject: [PATCH 4/8] Clean up animation layers related types --- .../NumericDataAnimationLayer/index.tsx | 69 ++++--------------- .../NumericDataAnimationLayer/types.d.ts | 13 ++-- .../NumericDataAnimationPaintLayer/index.tsx | 20 +++--- .../NumericDataAnimationPaintLayer/types.d.ts | 5 +- 4 files changed, 31 insertions(+), 76 deletions(-) diff --git a/src/layers/NumericDataAnimationLayer/index.tsx b/src/layers/NumericDataAnimationLayer/index.tsx index 177ade4..a43bfbf 100644 --- a/src/layers/NumericDataAnimationLayer/index.tsx +++ b/src/layers/NumericDataAnimationLayer/index.tsx @@ -1,12 +1,5 @@ -import { - CompositeLayer, - Layer, - LayersList, - LayerContext, - GetPickingInfoParams, -} from "deck.gl"; +import { CompositeLayer, Layer, LayersList } from "deck.gl"; import type { CompositeLayerProps, UpdateParameters } from "deck.gl"; -import type { BitmapLayerPickingInfo } from "@deck.gl/layers"; import type { Texture } from "@luma.gl/core"; import { NumericDataAnimationPaintLayer } from "../NumericDataAnimationPaintLayer"; @@ -27,53 +20,19 @@ const textureSamplerDefaultOption = { export default class NumericDataAnimationLayer extends CompositeLayer { static layerName: string = "numeric-data-animation-layer"; - - // initializeState(context: LayerContext): void { - // const { tileSize, textureParameters } = this.props; - // const dataTextureStart = context.device.createTexture({ - // data: this.props.imageDataStart, - // width: tileSize, - // height: tileSize, - // ...textureDefaultOption, - // sampler: { - // ...textureSamplerDefaultOption, - // ...textureParameters, - // }, - // }); - // const dataTextureEnd = context.device.createTexture({ - // data: this.props.imageDataEnd, - // width: tileSize, - // height: tileSize, - // ...textureDefaultOption, - // sampler: { - // ...textureSamplerDefaultOption, - // ...textureParameters, - // }, - // }); - // this.setState({ - // dataTextureStart, - // dataTextureEnd, - // }); - // } updateState( params: UpdateParameters< - Layer> + Layer> > ): void { const { props, oldProps, context } = params; - const { imageDataStart, imageDataEnd, timestamp } = props; - const { - imageDataStart: oldImageDataStart, - imageDataEnd: oldImageDataEnd, - timestamp: oldTimestamp, - } = oldProps; - if ( - imageDataStart !== oldImageDataStart && - imageDataEnd !== oldImageDataEnd - ) { + const { imageDataFrom, imageDataTo } = props; + const { imageDataFrom: oldImageDataFrom, imageDataTo: oldImageDataTo } = + oldProps; + if (imageDataFrom !== oldImageDataFrom && imageDataTo !== oldImageDataTo) { const { tileSize, textureParameters } = props; - const dataTextureStart = context.device.createTexture({ - data: this.props.imageDataStart, + const dataTextureFrom = context.device.createTexture({ + data: this.props.imageDataFrom, width: tileSize, height: tileSize, ...textureDefaultOption, @@ -82,8 +41,8 @@ export default class NumericDataAnimationLayer extends CompositeLayer; - imageDataEnd: TypedArray; -} - -export interface NumericDataPickingInfo extends BitmapLayerPickingInfo { - dataValue: NumberDataType | null; + extends NumericDataAnimationPaintLayerProps { + imageDataFrom: TypedArray; + imageDataTo: TypedArray; } diff --git a/src/layers/NumericDataAnimationPaintLayer/index.tsx b/src/layers/NumericDataAnimationPaintLayer/index.tsx index 71ae717..4deb93b 100644 --- a/src/layers/NumericDataAnimationPaintLayer/index.tsx +++ b/src/layers/NumericDataAnimationPaintLayer/index.tsx @@ -3,7 +3,7 @@ import { BitmapLayer } from "deck.gl"; import type { Texture } from "@luma.gl/core"; import type { ShaderModule } from "@luma.gl/shadertools"; -import type { NumericDataPaintLayerProps } from "./types"; +import type { NumericDataAnimationPaintLayerProps } from "./types"; const uniformBlock = `\ uniform ndUniforms { @@ -18,7 +18,7 @@ export type NDProps = { max: number; step: number; colormap_texture: Texture; - image_end: Texture; + image_to: Texture; }; const numericDataAnimationUniforms = { @@ -38,8 +38,9 @@ const defaultProps = { min: 0, max: 0, tileSize: 256, - timestamp: 0.0, + step: 0.0, imageData: [], + imageTo: [], colormap_image: { type: "image", value: null, @@ -47,7 +48,7 @@ const defaultProps = { }, }; -export class NumericDataAnimationPaintLayer extends BitmapLayer { +export class NumericDataAnimationPaintLayer extends BitmapLayer { static layerName = "numeric-paint-animation-layer"; static defaultProps = defaultProps; @@ -57,12 +58,11 @@ export class NumericDataAnimationPaintLayer extends BitmapLayer Date: Wed, 14 May 2025 12:08:08 -0400 Subject: [PATCH 5/8] Add more timestamps, Automate animation --- src/Animation.tsx | 104 +++++++++++++++---------------- src/components/ui/PlayButton.tsx | 5 ++ src/components/ui/Slider.tsx | 9 ++- src/components/ui/utils.ts | 53 ++++++++++++++++ src/zarr/index.ts | 7 +-- 5 files changed, 121 insertions(+), 57 deletions(-) create mode 100644 src/components/ui/PlayButton.tsx create mode 100644 src/components/ui/utils.ts diff --git a/src/Animation.tsx b/src/Animation.tsx index b94a72b..8fd18bc 100644 --- a/src/Animation.tsx +++ b/src/Animation.tsx @@ -1,23 +1,24 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Map as GLMap, NavigationControl, useControl, } from "react-map-gl/maplibre"; import { TileLayer } from "@deck.gl/geo-layers"; -import type { _TileLoadProps } from "@deck.gl/geo-layers"; +import type { _TileLoadProps, TileIndex } from "@deck.gl/geo-layers"; import { MapboxOverlay as DeckOverlay } from "@deck.gl/mapbox"; import ZarrReader from "./zarr"; import NumericDataAnimationLayer from "@/layers/NumericDataAnimationLayer"; -import type { NumericDataPickingInfo } from "@/layers/NumericDataLayer/types"; import Panel from "@/components/Panel"; import Description from "@/components/Description"; import Dropdown from "@/components/ui/Dropdown"; import RangeSlider from "@/components/ui/RangeSlider"; import SingleSlider from "@/components/ui/Slider"; -import CheckBox from "@/components/ui/Checkbox"; +import PlayButton from "@/components/ui/PlayButton"; + +import { usePausableAnimation } from "@/components/ui/utils"; import "maplibre-gl/dist/maplibre-gl.css"; import "./App.css"; @@ -45,7 +46,7 @@ const zarrReader = await ZarrReader.initialize({ }); const timestampUnit = 1; -const maxTimestamp = 2; +const maxTimestamp = 4; const quickCache = new Map(); @@ -56,6 +57,24 @@ function DeckGLOverlay(props) { return null; } +async function fetchOneTimeStamp({ + timestamp, + index, +}: { + timestamp: number; + index: TileIndex; +}) { + const { x, y, z } = index; + const keyName = `tile${timestamp}${x}${y}${z}`; + if (quickCache.get(keyName)) return quickCache.get(keyName); + const chunkData = await zarrReader.getTileData({ + ...index, + timestamp, + }); + quickCache.set(keyName, chunkData); + return chunkData; +} +const SPEED = 0.02; function App() { const [selectedColormap, setSelectedColormap] = useState("viridis"); const [minMax, setMinMax] = useState<{ min: number; max: number }>( @@ -67,18 +86,12 @@ function App() { Math.floor(timestamp + timestampUnit), maxTimestamp ); - const [showTooltip, setShowTooltip] = useState(false); - async function fetchOneTimeStamp({ timestamp, index }) { - const { x, y, z } = index; - const keyName = `tile${timestamp}${x}${y}${z}`; - const chunkData = await zarrReader.getTileData({ - ...index, - timestamp, - }); - quickCache.set(keyName, chunkData); - return chunkData; - } + const { isRunning, toggleAnimation } = usePausableAnimation(() => { + // Pass on a function to the setter of the state + // to make sure we always have the latest state + setTimestamp((prev) => (prev + SPEED) % maxTimestamp); + }); async function getTileData({ index, signal }: _TileLoadProps) { if (signal?.aborted) { @@ -89,45 +102,33 @@ function App() { const { min, max } = scale; const { x, y, z } = index; - let chunkDataStart; - let chunkDataEnd; + const timestampKeyStart = `tile${timestampStart}${x}${y}${z}`; const timestampKeyEnd = `tile${timestampEnd}${x}${y}${z}`; - // Make it synchronous when there the value is cached + // Make it synchronous when there are values cached if (quickCache.get(timestampKeyStart) && quickCache.get(timestampKeyEnd)) { return { - imageDataStart: quickCache.get(timestampKeyStart), - imageDataEnd: quickCache.get(timestampKeyEnd), + imageDataFrom: quickCache.get(timestampKeyStart), + imageDataTo: quickCache.get(timestampKeyEnd), min, max, }; } - const oneMoreStamp = Math.min(timestampEnd + 1, maxTimestamp); - await fetchOneTimeStamp({ timestamp: oneMoreStamp, index }); - - if (!quickCache.get(timestampKeyStart)) { - chunkDataStart = await fetchOneTimeStamp({ - index, - timestamp: timestampStart, - }); - } else { - chunkDataStart = quickCache.get(timestampKeyStart); - } + const chunkDataStart = await fetchOneTimeStamp({ + index, + timestamp: timestampStart, + }); - if (!quickCache.get(timestampKeyEnd)) { - chunkDataEnd = await fetchOneTimeStamp({ - index, - timestamp: timestampEnd, - }); - } else { - chunkDataEnd = quickCache.get(timestampKeyEnd); - } + const chunkDataEnd = await fetchOneTimeStamp({ + index, + timestamp: timestampEnd, + }); if (chunkDataStart && chunkDataEnd) { return { - imageDataStart: chunkDataStart, - imageDataEnd: chunkDataEnd, + imageDataFrom: chunkDataStart, + imageDataTo: chunkDataEnd, min, max, }; @@ -157,20 +158,20 @@ function App() { // onViewportLoad: null, // refinementStrategy: 'best-available', updateTriggers: { - getTileData: [timestampStart, timestampEnd], + getTileData: [timestampStart], renderSubLayers: [selectedColormap, minMax, timestamp], }, renderSubLayers: (props) => { - const { imageDataStart, imageDataEnd } = props.data; + const { imageDataFrom, imageDataTo } = props.data; const { boundingBox } = props.tile; return new NumericDataAnimationLayer(props, { data: undefined, colormap_image: `/colormaps/${selectedColormap}.png`, min: minMax.min, max: minMax.max, - imageDataStart, - imageDataEnd, - timestamp: timestamp - timestampStart, + imageDataFrom, + imageDataTo, + step: timestamp - timestampStart, tileSize: zarrReader.tileSize, bounds: [ boundingBox[0][0], @@ -201,9 +202,6 @@ function App() { const deckProps = { layers, - getTooltip: (info: NumericDataPickingInfo) => { - return showTooltip ? info.dataValue && `${info.dataValue}` : null; - }, }; return ( @@ -226,11 +224,13 @@ function App() { /> + ); diff --git a/src/components/ui/PlayButton.tsx b/src/components/ui/PlayButton.tsx new file mode 100644 index 0000000..4a8dd38 --- /dev/null +++ b/src/components/ui/PlayButton.tsx @@ -0,0 +1,5 @@ +import { Button } from "@chakra-ui/react"; + +export default function PlayButton({ onPlay, onClick }) { + return ; +} diff --git a/src/components/ui/Slider.tsx b/src/components/ui/Slider.tsx index 04b7970..3fe8527 100644 --- a/src/components/ui/Slider.tsx +++ b/src/components/ui/Slider.tsx @@ -4,6 +4,7 @@ export type SliderUIProps = { minMax: [number, number]; step?: number; label?: string; + currentValue: number; onValueChange: (param: number) => void; }; @@ -11,6 +12,7 @@ export default function SingleSlider({ minMax, step, label, + currentValue, onValueChange, }: SliderUIProps) { const handleChange = (detail: Slider.ValueChangeDetails) => { @@ -21,6 +23,7 @@ export default function SingleSlider({ defaultValue={[minMax[0]]} min={minMax[0]} max={minMax[1]} + value={[currentValue]} maxW="100%" width="100%" step={step} @@ -32,7 +35,11 @@ export default function SingleSlider({ - + idx)} + /> ); diff --git a/src/components/ui/utils.ts b/src/components/ui/utils.ts new file mode 100644 index 0000000..755b874 --- /dev/null +++ b/src/components/ui/utils.ts @@ -0,0 +1,53 @@ +import { useState, useRef, useEffect } from "react"; + +// Define the callback function type +type AnimationFrameCallback = (deltaTime: number) => void; + +// Define the return type for our hook +interface PausableAnimationControls { + isRunning: boolean; + startAnimation: () => void; + stopAnimation: () => void; + toggleAnimation: () => void; +} + +/** + * A custom hook that provides requestAnimationFrame with pause/resume functionality + * @param callback Function to call on each animation frame with deltaTime parameter + * @returns Object with animation control functions and state + */ +export function usePausableAnimation( + callback: AnimationFrameCallback +): PausableAnimationControls { + // Use useRef for mutable variables that we want to persist + // without triggering a re-render on their change + const requestRef = useRef(null); + const previousTimeRef = useRef(undefined); + // Add a state to control whether the animation is running + const [isRunning, setIsRunning] = useState(false); + + const animate = (time: number): void => { + if (previousTimeRef.current !== undefined && isRunning) { + const deltaTime = time - previousTimeRef.current; + callback(deltaTime); + } + previousTimeRef.current = time; + requestRef.current = requestAnimationFrame(animate); + }; + + useEffect(() => { + requestRef.current = requestAnimationFrame(animate); + return () => { + if (requestRef.current !== null) { + cancelAnimationFrame(requestRef.current); + } + }; + }, [isRunning]); // Re-run when isRunning changes + + // Return functions to start and stop the animation + const startAnimation = (): void => setIsRunning(true); + const stopAnimation = (): void => setIsRunning(false); + const toggleAnimation = (): void => setIsRunning((prev) => !prev); + + return { isRunning, startAnimation, stopAnimation, toggleAnimation }; +} diff --git a/src/zarr/index.ts b/src/zarr/index.ts index 969a031..a980e9e 100644 --- a/src/zarr/index.ts +++ b/src/zarr/index.ts @@ -117,14 +117,13 @@ export default class ZarrReader { }); if (arr.is("number")) { - const { data } = await arr.getChunk([timestamp, y, x]); + const { data } = await arr.getChunk([this._t, y, x]); // @TODO : remove once the data has actual timestamps - if (timestamp == 1) { - const tempArray = new Float32Array(this.tileSize * this.tileSize * 2); + if (timestamp % 2 == 1) { + const tempArray = new Float32Array(this.tileSize * this.tileSize); tempArray.fill(this.scale.min); return tempArray; } - return data; } else { return undefined; From 7fdc9fbba632cd00e7b3370e19fab9f86854d5f6 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 14 May 2025 12:27:29 -0400 Subject: [PATCH 6/8] Fix types related to UI --- src/Animation.tsx | 19 +++++++++++-------- src/App.tsx | 1 + src/components/ui/PlayButton.tsx | 7 ++++++- src/components/ui/RangeSlider.tsx | 5 ++++- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Animation.tsx b/src/Animation.tsx index 8fd18bc..38c0627 100644 --- a/src/Animation.tsx +++ b/src/Animation.tsx @@ -5,7 +5,7 @@ import { useControl, } from "react-map-gl/maplibre"; import { TileLayer } from "@deck.gl/geo-layers"; -import type { _TileLoadProps, TileIndex } from "@deck.gl/geo-layers"; +import type { _TileLoadProps } from "@deck.gl/geo-layers"; import { MapboxOverlay as DeckOverlay } from "@deck.gl/mapbox"; @@ -45,8 +45,11 @@ const zarrReader = await ZarrReader.initialize({ varName: VAR_NAME, }); -const timestampUnit = 1; -const maxTimestamp = 4; +export type TileIndex = { x: number; y: number; z: number }; + +const TIME_UNIT = 1; +const MAX_TIMESTAMP = 4; +const SPEED = 0.02; const quickCache = new Map(); @@ -74,7 +77,7 @@ async function fetchOneTimeStamp({ quickCache.set(keyName, chunkData); return chunkData; } -const SPEED = 0.02; + function App() { const [selectedColormap, setSelectedColormap] = useState("viridis"); const [minMax, setMinMax] = useState<{ min: number; max: number }>( @@ -83,14 +86,14 @@ function App() { const [timestamp, setTimestamp] = useState(0.0); const timestampStart = Math.floor(timestamp); const timestampEnd = Math.min( - Math.floor(timestamp + timestampUnit), - maxTimestamp + Math.floor(timestamp + TIME_UNIT), + MAX_TIMESTAMP ); const { isRunning, toggleAnimation } = usePausableAnimation(() => { // Pass on a function to the setter of the state // to make sure we always have the latest state - setTimestamp((prev) => (prev + SPEED) % maxTimestamp); + setTimestamp((prev) => (prev + SPEED) % MAX_TIMESTAMP); }); async function getTileData({ index, signal }: _TileLoadProps) { @@ -224,7 +227,7 @@ function App() { /> void; +}; + +export default function PlayButton({ onPlay, onClick }: PlayButtonProps) { return ; } diff --git a/src/components/ui/RangeSlider.tsx b/src/components/ui/RangeSlider.tsx index 9242d5b..b00f57f 100644 --- a/src/components/ui/RangeSlider.tsx +++ b/src/components/ui/RangeSlider.tsx @@ -1,7 +1,10 @@ import { Slider } from "@chakra-ui/react"; import type { SliderUIProps } from "./Slider"; -type RangeSliderUIProps = SliderUIProps & { +type RangeSliderUIProps = Omit< + SliderUIProps, + "onValueChange" | "currentValue" +> & { onValueChange: (param: { min: number; max: number }) => void; }; From c3986a2a3ab88119fe52c2fff7c91e4f41423170 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Wed, 14 May 2025 12:35:28 -0400 Subject: [PATCH 7/8] Make react router work with Netlify --- package-lock.json | 111 +++++++++++++++++++++++++++++++++++++++++----- package.json | 1 + public/_redirects | 1 + vite.config.ts | 3 +- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 public/_redirects diff --git a/package-lock.json b/package-lock.json index d195e8e..2b94ad5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "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", @@ -2098,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", @@ -2180,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", @@ -4454,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", @@ -5792,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", @@ -6682,13 +6745,12 @@ } }, "node_modules/react-router": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.3.tgz", - "integrity": "sha512-3iUDM4/fZCQ89SXlDa+Ph3MevBrozBAI655OAfWQlTm9nBR0IKlrmNwFow5lPHttbwvITZfkeeeZFP6zt3F7pw==", + "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", - "turbo-stream": "2.4.0" + "set-cookie-parser": "^2.6.0" }, "engines": { "node": ">=20.0.0" @@ -6973,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", @@ -7029,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", @@ -7263,11 +7350,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7340,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", diff --git a/package.json b/package.json index 45c8f16..db5e459 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "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/vite.config.ts b/vite.config.ts index 357971a..fb0fe1c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,11 @@ import { fileURLToPath, URL } from "url"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +import netlifyPlugin from "@netlify/vite-plugin-react-router"; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), netlifyPlugin()], build: { target: "ES2022", }, From aaafe959ae304f3586053f0c40069dec50ef21a2 Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Fri, 16 May 2025 15:24:01 -0400 Subject: [PATCH 8/8] Add zoom to animation --- src/Animation.tsx | 13 ++++--------- src/App.tsx | 2 +- src/zarr/index.ts | 1 + 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Animation.tsx b/src/Animation.tsx index 38c0627..bb09350 100644 --- a/src/Animation.tsx +++ b/src/Animation.tsx @@ -20,16 +20,11 @@ import PlayButton from "@/components/ui/PlayButton"; import { usePausableAnimation } from "@/components/ui/utils"; +import { INITIAL_VIEW_STATE } from "./App"; + import "maplibre-gl/dist/maplibre-gl.css"; import "./App.css"; -const INITIAL_VIEW_STATE = { - latitude: 51.47, - longitude: 0.45, - zoom: 0, - maxZoom: 1, -}; - const MAP_STYLE = "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"; @@ -153,8 +148,8 @@ function App() { // maxCacheByteSize: null, // maxCacheSize: null, // maxRequests: 6, - // maxZoom: 19, - // minZoom: 0, + maxZoom: zarrReader.maxZoom, + minZoom: zarrReader.minZoom, // onTileError: null, // onTileLoad: null, // onTileUnload: null, diff --git a/src/App.tsx b/src/App.tsx index 406c07c..992ec36 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,7 @@ import CheckBox from "@/components/ui/Checkbox"; import "maplibre-gl/dist/maplibre-gl.css"; import "./App.css"; -const INITIAL_VIEW_STATE = { +export const INITIAL_VIEW_STATE = { latitude: 51.47, longitude: 0.45, zoom: 0, diff --git a/src/zarr/index.ts b/src/zarr/index.ts index a980e9e..a4c4492 100644 --- a/src/zarr/index.ts +++ b/src/zarr/index.ts @@ -108,6 +108,7 @@ export default class ZarrReader { async getTileData({ x, y, + z, timestamp, }: TileIndex & { timestamp: number }): Promise< TypedArray | undefined