Skip to content

Commit 7ce69e3

Browse files
committed
Add more timestamps, Automate animation
1 parent 7df1de4 commit 7ce69e3

File tree

5 files changed

+121
-57
lines changed

5 files changed

+121
-57
lines changed

src/Animation.tsx

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import { useState, useEffect } from "react";
1+
import { useState } from "react";
22
import {
33
Map as GLMap,
44
NavigationControl,
55
useControl,
66
} from "react-map-gl/maplibre";
77
import { TileLayer } from "@deck.gl/geo-layers";
8-
import type { _TileLoadProps } from "@deck.gl/geo-layers";
8+
import type { _TileLoadProps, TileIndex } from "@deck.gl/geo-layers";
99

1010
import { MapboxOverlay as DeckOverlay } from "@deck.gl/mapbox";
1111

1212
import ZarrReader from "./zarr";
1313
import NumericDataAnimationLayer from "@/layers/NumericDataAnimationLayer";
14-
import type { NumericDataPickingInfo } from "@/layers/NumericDataLayer/types";
1514
import Panel from "@/components/Panel";
1615
import Description from "@/components/Description";
1716
import Dropdown from "@/components/ui/Dropdown";
1817
import RangeSlider from "@/components/ui/RangeSlider";
1918
import SingleSlider from "@/components/ui/Slider";
20-
import CheckBox from "@/components/ui/Checkbox";
19+
import PlayButton from "@/components/ui/PlayButton";
20+
21+
import { usePausableAnimation } from "@/components/ui/utils";
2122

2223
import "maplibre-gl/dist/maplibre-gl.css";
2324
import "./App.css";
@@ -45,7 +46,7 @@ const zarrReader = await ZarrReader.initialize({
4546
});
4647

4748
const timestampUnit = 1;
48-
const maxTimestamp = 2;
49+
const maxTimestamp = 4;
4950

5051
const quickCache = new Map();
5152

@@ -56,6 +57,24 @@ function DeckGLOverlay(props) {
5657
return null;
5758
}
5859

60+
async function fetchOneTimeStamp({
61+
timestamp,
62+
index,
63+
}: {
64+
timestamp: number;
65+
index: TileIndex;
66+
}) {
67+
const { x, y, z } = index;
68+
const keyName = `tile${timestamp}${x}${y}${z}`;
69+
if (quickCache.get(keyName)) return quickCache.get(keyName);
70+
const chunkData = await zarrReader.getTileData({
71+
...index,
72+
timestamp,
73+
});
74+
quickCache.set(keyName, chunkData);
75+
return chunkData;
76+
}
77+
const SPEED = 0.02;
5978
function App() {
6079
const [selectedColormap, setSelectedColormap] = useState<string>("viridis");
6180
const [minMax, setMinMax] = useState<{ min: number; max: number }>(
@@ -67,18 +86,12 @@ function App() {
6786
Math.floor(timestamp + timestampUnit),
6887
maxTimestamp
6988
);
70-
const [showTooltip, setShowTooltip] = useState<boolean>(false);
7189

72-
async function fetchOneTimeStamp({ timestamp, index }) {
73-
const { x, y, z } = index;
74-
const keyName = `tile${timestamp}${x}${y}${z}`;
75-
const chunkData = await zarrReader.getTileData({
76-
...index,
77-
timestamp,
78-
});
79-
quickCache.set(keyName, chunkData);
80-
return chunkData;
81-
}
90+
const { isRunning, toggleAnimation } = usePausableAnimation(() => {
91+
// Pass on a function to the setter of the state
92+
// to make sure we always have the latest state
93+
setTimestamp((prev) => (prev + SPEED) % maxTimestamp);
94+
});
8295

8396
async function getTileData({ index, signal }: _TileLoadProps) {
8497
if (signal?.aborted) {
@@ -89,45 +102,33 @@ function App() {
89102

90103
const { min, max } = scale;
91104
const { x, y, z } = index;
92-
let chunkDataStart;
93-
let chunkDataEnd;
105+
94106
const timestampKeyStart = `tile${timestampStart}${x}${y}${z}`;
95107
const timestampKeyEnd = `tile${timestampEnd}${x}${y}${z}`;
96-
// Make it synchronous when there the value is cached
108+
// Make it synchronous when there are values cached
97109
if (quickCache.get(timestampKeyStart) && quickCache.get(timestampKeyEnd)) {
98110
return {
99-
imageDataStart: quickCache.get(timestampKeyStart),
100-
imageDataEnd: quickCache.get(timestampKeyEnd),
111+
imageDataFrom: quickCache.get(timestampKeyStart),
112+
imageDataTo: quickCache.get(timestampKeyEnd),
101113
min,
102114
max,
103115
};
104116
}
105117

106-
const oneMoreStamp = Math.min(timestampEnd + 1, maxTimestamp);
107-
await fetchOneTimeStamp({ timestamp: oneMoreStamp, index });
108-
109-
if (!quickCache.get(timestampKeyStart)) {
110-
chunkDataStart = await fetchOneTimeStamp({
111-
index,
112-
timestamp: timestampStart,
113-
});
114-
} else {
115-
chunkDataStart = quickCache.get(timestampKeyStart);
116-
}
118+
const chunkDataStart = await fetchOneTimeStamp({
119+
index,
120+
timestamp: timestampStart,
121+
});
117122

118-
if (!quickCache.get(timestampKeyEnd)) {
119-
chunkDataEnd = await fetchOneTimeStamp({
120-
index,
121-
timestamp: timestampEnd,
122-
});
123-
} else {
124-
chunkDataEnd = quickCache.get(timestampKeyEnd);
125-
}
123+
const chunkDataEnd = await fetchOneTimeStamp({
124+
index,
125+
timestamp: timestampEnd,
126+
});
126127

127128
if (chunkDataStart && chunkDataEnd) {
128129
return {
129-
imageDataStart: chunkDataStart,
130-
imageDataEnd: chunkDataEnd,
130+
imageDataFrom: chunkDataStart,
131+
imageDataTo: chunkDataEnd,
131132
min,
132133
max,
133134
};
@@ -157,20 +158,20 @@ function App() {
157158
// onViewportLoad: null,
158159
// refinementStrategy: 'best-available',
159160
updateTriggers: {
160-
getTileData: [timestampStart, timestampEnd],
161+
getTileData: [timestampStart],
161162
renderSubLayers: [selectedColormap, minMax, timestamp],
162163
},
163164
renderSubLayers: (props) => {
164-
const { imageDataStart, imageDataEnd } = props.data;
165+
const { imageDataFrom, imageDataTo } = props.data;
165166
const { boundingBox } = props.tile;
166167
return new NumericDataAnimationLayer(props, {
167168
data: undefined,
168169
colormap_image: `/colormaps/${selectedColormap}.png`,
169170
min: minMax.min,
170171
max: minMax.max,
171-
imageDataStart,
172-
imageDataEnd,
173-
timestamp: timestamp - timestampStart,
172+
imageDataFrom,
173+
imageDataTo,
174+
step: timestamp - timestampStart,
174175
tileSize: zarrReader.tileSize,
175176
bounds: [
176177
boundingBox[0][0],
@@ -201,9 +202,6 @@ function App() {
201202

202203
const deckProps = {
203204
layers,
204-
getTooltip: (info: NumericDataPickingInfo) => {
205-
return showTooltip ? info.dataValue && `${info.dataValue}` : null;
206-
},
207205
};
208206

209207
return (
@@ -226,11 +224,13 @@ function App() {
226224
/>
227225

228226
<SingleSlider
229-
minMax={[0, 2]}
230-
step={0.02}
227+
minMax={[0, maxTimestamp]}
228+
step={SPEED}
229+
currentValue={timestamp}
231230
label="Timestamp"
232231
onValueChange={setTimestamp}
233232
/>
233+
<PlayButton onPlay={isRunning} onClick={toggleAnimation} />
234234
</Panel>
235235
</>
236236
);

src/components/ui/PlayButton.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Button } from "@chakra-ui/react";
2+
3+
export default function PlayButton({ onPlay, onClick }) {
4+
return <Button onClick={onClick}>{onPlay ? "Pause" : "Play"}</Button>;
5+
}

src/components/ui/Slider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ export type SliderUIProps = {
44
minMax: [number, number];
55
step?: number;
66
label?: string;
7+
currentValue: number;
78
onValueChange: (param: number) => void;
89
};
910

1011
export default function SingleSlider({
1112
minMax,
1213
step,
1314
label,
15+
currentValue,
1416
onValueChange,
1517
}: SliderUIProps) {
1618
const handleChange = (detail: Slider.ValueChangeDetails) => {
@@ -21,6 +23,7 @@ export default function SingleSlider({
2123
defaultValue={[minMax[0]]}
2224
min={minMax[0]}
2325
max={minMax[1]}
26+
value={[currentValue]}
2427
maxW="100%"
2528
width="100%"
2629
step={step}
@@ -32,7 +35,11 @@ export default function SingleSlider({
3235
<Slider.Range />
3336
</Slider.Track>
3437
<Slider.Thumb index={0} boxSize={6} shadow="md"></Slider.Thumb>
35-
<Slider.Marks marks={[0, 1, 2]} />
38+
<Slider.Marks
39+
marks={new Array(minMax[1] - minMax[0] + 1)
40+
.fill(0)
41+
.map((_e, idx) => idx)}
42+
/>
3643
</Slider.Control>
3744
</Slider.Root>
3845
);

src/components/ui/utils.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useState, useRef, useEffect } from "react";
2+
3+
// Define the callback function type
4+
type AnimationFrameCallback = (deltaTime: number) => void;
5+
6+
// Define the return type for our hook
7+
interface PausableAnimationControls {
8+
isRunning: boolean;
9+
startAnimation: () => void;
10+
stopAnimation: () => void;
11+
toggleAnimation: () => void;
12+
}
13+
14+
/**
15+
* A custom hook that provides requestAnimationFrame with pause/resume functionality
16+
* @param callback Function to call on each animation frame with deltaTime parameter
17+
* @returns Object with animation control functions and state
18+
*/
19+
export function usePausableAnimation(
20+
callback: AnimationFrameCallback
21+
): PausableAnimationControls {
22+
// Use useRef for mutable variables that we want to persist
23+
// without triggering a re-render on their change
24+
const requestRef = useRef<number | null>(null);
25+
const previousTimeRef = useRef<number | undefined>(undefined);
26+
// Add a state to control whether the animation is running
27+
const [isRunning, setIsRunning] = useState<boolean>(false);
28+
29+
const animate = (time: number): void => {
30+
if (previousTimeRef.current !== undefined && isRunning) {
31+
const deltaTime = time - previousTimeRef.current;
32+
callback(deltaTime);
33+
}
34+
previousTimeRef.current = time;
35+
requestRef.current = requestAnimationFrame(animate);
36+
};
37+
38+
useEffect(() => {
39+
requestRef.current = requestAnimationFrame(animate);
40+
return () => {
41+
if (requestRef.current !== null) {
42+
cancelAnimationFrame(requestRef.current);
43+
}
44+
};
45+
}, [isRunning]); // Re-run when isRunning changes
46+
47+
// Return functions to start and stop the animation
48+
const startAnimation = (): void => setIsRunning(true);
49+
const stopAnimation = (): void => setIsRunning(false);
50+
const toggleAnimation = (): void => setIsRunning((prev) => !prev);
51+
52+
return { isRunning, startAnimation, stopAnimation, toggleAnimation };
53+
}

src/zarr/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,13 @@ export default class ZarrReader {
117117
});
118118

119119
if (arr.is("number")) {
120-
const { data } = await arr.getChunk([timestamp, y, x]);
120+
const { data } = await arr.getChunk([this._t, y, x]);
121121
// @TODO : remove once the data has actual timestamps
122-
if (timestamp == 1) {
123-
const tempArray = new Float32Array(this.tileSize * this.tileSize * 2);
122+
if (timestamp % 2 == 1) {
123+
const tempArray = new Float32Array(this.tileSize * this.tileSize);
124124
tempArray.fill(this.scale.min);
125125
return tempArray;
126126
}
127-
128127
return data;
129128
} else {
130129
return undefined;

0 commit comments

Comments
 (0)