@@ -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