Skip to content

Commit 62733f1

Browse files
authored
Merge pull request #60 from firelab/slider-timescale
adjust slider mark postions to correspond to frame timestamp
2 parents 2e913ce + 84e9429 commit 62733f1

1 file changed

Lines changed: 75 additions & 22 deletions

File tree

frontend/src/App.jsx

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default function App() {
4040

4141
// Playback State
4242
const [currentFrameIndex, setCurrentFrameIndex] = useState(0);
43+
const [sliderValue, setSliderValue] = useState(0);
4344
const [isPlaying, setIsPlaying] = useState(false);
4445
const playbackRef = useRef(null);
4546

@@ -125,29 +126,49 @@ export default function App() {
125126
};
126127

127128
// fetch frames once the job is completed and the jobId and numFrames are set
128-
useEffect(() => {
129-
async function fetchFrames() {
130-
if (jobStatus !== "COMPLETED" || !jobId || numFrames <= 0) return;
131-
console.log(`attempting to fetch ${numFrames} frames for job ${jobId}`);
132-
try {
133-
const promises = Array.from({ length: numFrames }, async (_, i) => {
134-
const res = await fetch(`/apis/jobs/${jobId}/frames/${i}`);
135-
if (res.status === 404) {
129+
useEffect(() => {
130+
async function fetchFrames() {
131+
if (jobStatus !== "COMPLETED" || !jobId || numFrames <= 0) return;
132+
console.log(`attempting to fetch ${numFrames} frames for job ${jobId}`);
133+
try {
134+
const promises = Array.from({ length: numFrames }, async (_, i) => {
135+
const res = await fetch(`/apis/jobs/${jobId}/frames/${i}`);
136+
if (res.status === 404) {
136137
console.warn(`Frame ${i} gave 404 - skipping`);
137138
return null;
138139
}
139140
if (!res.ok) throw new Error(`Failed frame ${i}`);
140141
const timestamp = res.headers.get("x-frame-timestamp");
142+
const isForecast = res.headers.get("x-frame-is-forecast");
141143
const blob = await res.blob();
142144
return {
143145
url: URL.createObjectURL(blob),
144146
timestamp,
147+
isForecast,
145148
index: i,
146149
};
147150
});
148151
const frames = await Promise.all(promises);
149-
frames.filter(Boolean).sort((a, b) => a.index - b.index);
150-
setFrames(frames);
152+
153+
const processedFrames = frames
154+
.filter(Boolean)
155+
.sort((a, b) => a.index - b.index);
156+
157+
const firstTime = dayjs(processedFrames[0].timestamp).valueOf();
158+
const lastTime = dayjs(processedFrames[processedFrames.length - 1].timestamp).valueOf();
159+
const totalSpan = lastTime - firstTime || 1;
160+
161+
const sliderFrames = processedFrames.map((frame) => {
162+
const t = dayjs(frame.timestamp).valueOf();
163+
const relative = ((t - firstTime) / totalSpan) * 100;
164+
165+
return {
166+
...frame,
167+
sliderValue: relative,
168+
};
169+
});
170+
171+
setFrames(sliderFrames);
151172
setIsPlaying(frames.length > 0);
152173
console.log("Frames fetched successfully: ", frames);
153174
} catch (err) {
@@ -211,21 +232,44 @@ useEffect(() => {
211232

212233
// Handle Playback
213234
useEffect(() => {
214-
if (isPlaying) {
235+
if (isPlaying && frames.length > 0) {
215236
playbackRef.current = setInterval(() => {
216-
setCurrentFrameIndex((prev) => (prev + 1) % frames.length);
237+
setCurrentFrameIndex((prev) => {
238+
const nextIndex = (prev + 1) % frames.length;
239+
setSliderValue(frames[nextIndex].sliderValue);
240+
return nextIndex;
241+
});
217242
}, 300);
218243
} else {
219244
clearInterval(playbackRef.current);
220245
}
221-
222246
return () => clearInterval(playbackRef.current);
223-
}, [isPlaying, frames.length]);
247+
}, [isPlaying, frames]);
224248

225249
// Handle Slider Change
226250
const handleSliderChange = (event, newValue) => {
227251
setIsPlaying(false);
228-
setCurrentFrameIndex(newValue);
252+
const { frame, index } = getNearestFrame(newValue);
253+
if (!frame) return;
254+
setSliderValue(frame.sliderValue);
255+
setCurrentFrameIndex(index);
256+
};
257+
258+
const getNearestFrame = (val) => {
259+
if (!frames.length) return { frame: null, index: 0 };
260+
let nearestIndex = 0;
261+
let nearestDistance = Math.abs(frames[0].sliderValue - val);
262+
frames.forEach((frame, i) => {
263+
const dist = Math.abs(frame.sliderValue - val);
264+
if (dist < nearestDistance) {
265+
nearestDistance = dist;
266+
nearestIndex = i;
267+
}
268+
});
269+
return {
270+
frame: frames[nearestIndex],
271+
index: nearestIndex,
272+
};
229273
};
230274

231275
// ---------------------------------------- JSX ----------------------------------------
@@ -340,9 +384,9 @@ useEffect(() => {
340384
{/* Playback Controls */}
341385
{/* The CSS is a little cursed. */}
342386
<div className="flex h-full items-end">
343-
<div className=" flex md:min-w-[calc(100vw-1rem)] md:pl-92 z-999 w-full">
387+
<div className=" flex md:min-w-[calc(100vw-1rem)] md:pl-92 z-998 w-full">
344388
{numFrames !== 0 && (
345-
<div className="flex flex-col items-center w-full max-w-[800px] bg-white p-2 rounded-xl md:shadow-2xl md:mr-4 md:pr-4">
389+
<div className="flex flex-col items-center w-full max-w-[1200px] bg-white p-2 rounded-xl md:shadow-2xl md:mr-4 md:pr-4">
346390
<div className="flex w-full items-center">
347391
<button
348392
type="button"
@@ -352,13 +396,22 @@ useEffect(() => {
352396
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
353397
</button>
354398
<Slider
355-
value={currentFrameIndex}
399+
value={sliderValue}
356400
min={0}
357-
max={frames.length > 0 ? frames.length - 1 : 0}
358-
step={1}
401+
max={100}
402+
step={null}
359403
onChange={handleSliderChange}
360404
valueLabelDisplay="auto"
361-
marks={frames.map((_, i) => ({ value: i }))}
405+
valueLabelFormat={() =>
406+
frames[currentFrameIndex]?.timestamp
407+
? dayjs(frames[currentFrameIndex].timestamp)
408+
.tz(timezone)
409+
.format("YYYY-MM-DD HH:mm z")
410+
: "No timestamp"
411+
}
412+
marks={frames.map((frame, i) => ({
413+
value: frame.sliderValue,
414+
}))}
362415
/>
363416
</div>
364417
<div className="flex w-full pt-4">
@@ -411,7 +464,7 @@ useEffect(() => {
411464
</div>
412465
</div>
413466
</div>
414-
<footer className=" m-4 absolute bottom-0 left-0 hidden md:block shadow-xl hover:shadow-sm transition-all">
467+
<footer className="z-999 m-4 absolute bottom-0 left-0 hidden md:block shadow-xl hover:shadow-sm transition-all">
415468
<a className="outline-1 hover:text-black opacity-50 hover:opacity-100 transition-all rounded-md p-2 flex gap-2 items-center" href="https://github.com/firelab/gust-front-detection-webapp" target="_blank" rel="noopener noreferrer">
416469
<p className="">Code</p>
417470
<img src="/assets/github.svg" alt="GitHub" className="w-6 h-6" />

0 commit comments

Comments
 (0)