@@ -237,6 +237,196 @@ L.Control.Screenshot = L.Control.extend({
237237// Add screenshot control to map
238238map . addControl ( new L . Control . Screenshot ( ) ) ;
239239
240+ // Add GIF recording control
241+ L . Control . GIFRecorder = L . Control . extend ( {
242+ options : {
243+ position : 'topleft'
244+ } ,
245+
246+ onAdd : function ( map ) {
247+ const container = L . DomUtil . create ( 'div' , 'leaflet-control-gif' ) ;
248+ const button = L . DomUtil . create ( 'a' , 'leaflet-control-gif-button' , container ) ;
249+
250+ button . innerHTML = '🎥' ;
251+ button . href = '#' ;
252+ button . title = 'Record GIF' ;
253+ button . style . cssText = `
254+ width: 26px;
255+ height: 26px;
256+ line-height: 26px;
257+ display: block;
258+ text-align: center;
259+ text-decoration: none;
260+ color: black;
261+ background: white;
262+ border: 2px solid rgba(0,0,0,0.2);
263+ border-radius: 4px;
264+ box-shadow: 0 1px 5px rgba(0,0,0,0.4);
265+ font-size: 14px;
266+ margin-bottom: 5px;
267+ ` ;
268+
269+ L . DomEvent . on ( button , 'click' , L . DomEvent . stopPropagation )
270+ . on ( button , 'click' , L . DomEvent . preventDefault )
271+ . on ( button , 'click' , this . _startRecording , this ) ;
272+
273+ this . button = button ;
274+ return container ;
275+ } ,
276+
277+ _startRecording : async function ( ) {
278+ if ( ! densities || densities . length === 0 ) {
279+ alert ( 'Please load data first.' ) ;
280+ return ;
281+ }
282+
283+ // Pause playback if active
284+ const playBtn = document . getElementById ( 'playBtn' ) ;
285+ if ( playBtn && playBtn . textContent === '⏸' ) {
286+ playBtn . click ( ) ;
287+ }
288+
289+ const fpsInput = document . getElementById ( 'fpsInput' ) ;
290+ const fps = parseFloat ( fpsInput . value ) || 10 ;
291+
292+ if ( ! confirm ( `Start recording GIF from current time to end?\nFPS: ${ fps } \nNote: This process may take a while.` ) ) {
293+ return ;
294+ }
295+
296+ let isRecording = true ;
297+
298+ // Show loading/progress indicator
299+ const loadingDiv = document . createElement ( 'div' ) ;
300+ loadingDiv . id = 'gif-progress' ;
301+ loadingDiv . style . cssText = `
302+ position: fixed;
303+ top: 50%;
304+ left: 50%;
305+ transform: translate(-50%, -50%);
306+ background: rgba(0,0,0,0.8);
307+ color: white;
308+ padding: 20px;
309+ border-radius: 10px;
310+ z-index: 10000;
311+ font-size: 16px;
312+ text-align: center;
313+ ` ;
314+ loadingDiv . innerHTML = 'Initializing GIF recorder...<br>' ;
315+
316+ // Add Stop button
317+ const stopBtn = document . createElement ( 'button' ) ;
318+ stopBtn . textContent = 'Stop & Save' ;
319+ stopBtn . style . cssText = 'margin-top: 10px; padding: 5px 10px; cursor: pointer;' ;
320+ stopBtn . onclick = ( ) => {
321+ isRecording = false ;
322+ stopBtn . disabled = true ;
323+ stopBtn . textContent = 'Stopping...' ;
324+ } ;
325+ loadingDiv . appendChild ( stopBtn ) ;
326+
327+ document . body . appendChild ( loadingDiv ) ;
328+
329+ // Initialize GIF
330+ // Create a blob for the worker to avoid cross-origin issues
331+ const workerBlob = new Blob ( [ `importScripts('https://cdnjs.cloudflare.com/ajax/libs/gif.js/0.2.0/gif.worker.js');` ] , { type : 'application/javascript' } ) ;
332+ const workerUrl = URL . createObjectURL ( workerBlob ) ;
333+
334+ const gif = new GIF ( {
335+ workers : 2 ,
336+ quality : 10 ,
337+ width : map . getSize ( ) . x ,
338+ height : map . getSize ( ) . y ,
339+ workerScript : workerUrl
340+ } ) ;
341+
342+ const timeSlider = document . getElementById ( 'timeSlider' ) ;
343+ const startVal = parseInt ( timeSlider . value ) ;
344+ const maxVal = parseInt ( timeSlider . max ) ;
345+ const step = parseInt ( timeSlider . step ) ;
346+
347+ let currentVal = startVal ;
348+
349+ gif . on ( 'finished' , function ( blob ) {
350+ if ( document . getElementById ( 'gif-progress' ) ) {
351+ document . body . removeChild ( loadingDiv ) ;
352+ }
353+ window . URL . revokeObjectURL ( workerUrl ) ;
354+
355+ const url = URL . createObjectURL ( blob ) ;
356+ const a = document . createElement ( 'a' ) ;
357+ a . href = url ;
358+ a . download = `simulation_${ new Date ( ) . toISOString ( ) . slice ( 0 , 19 ) . replace ( / : / g, '-' ) } .gif` ;
359+ document . body . appendChild ( a ) ;
360+ a . click ( ) ;
361+ document . body . removeChild ( a ) ;
362+ } ) ;
363+
364+ gif . on ( 'progress' , function ( p ) {
365+ if ( document . getElementById ( 'gif-progress' ) ) {
366+ // Keep the stop button if rendering hasn't finished, but usually rendering is blocking or fast enough.
367+ // Actually gif.js renders in workers.
368+ // We can just update the text part.
369+ loadingDiv . firstChild . textContent = `Rendering GIF: ${ Math . round ( p * 100 ) } %` ;
370+ }
371+ } ) ;
372+
373+ // Capture loop
374+ const captureFrame = async ( ) => {
375+ if ( ! isRecording || currentVal > maxVal ) {
376+ loadingDiv . innerHTML = 'Rendering GIF...' ;
377+ gif . render ( ) ;
378+ return ;
379+ }
380+
381+ // Update slider and map
382+ timeSlider . value = currentVal ;
383+ timeSlider . dispatchEvent ( new Event ( 'input' ) ) ;
384+
385+ // Update progress text
386+ const progress = Math . round ( ( ( currentVal - startVal ) / ( maxVal - startVal ) ) * 100 ) ;
387+
388+ // Clear previous content but keep the stop button
389+ loadingDiv . innerHTML = '' ;
390+ loadingDiv . appendChild ( document . createTextNode ( `Capturing frames: ${ progress } %` ) ) ;
391+ loadingDiv . appendChild ( document . createElement ( 'br' ) ) ;
392+ loadingDiv . appendChild ( document . createTextNode ( `Time: ${ document . getElementById ( 'timeLabel' ) . textContent } ` ) ) ;
393+ loadingDiv . appendChild ( document . createElement ( 'br' ) ) ;
394+ loadingDiv . appendChild ( stopBtn ) ; // Re-append button to keep it at bottom
395+
396+ // Wait a bit for render (though input event is sync, leaflet/canvas might need a tick)
397+ await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
398+
399+ try {
400+ const canvas = await html2canvas ( document . getElementById ( 'map' ) , {
401+ useCORS : true ,
402+ allowTaint : false ,
403+ logging : false ,
404+ scale : 1 // Use 1 for GIF to keep size reasonable
405+ } ) ;
406+
407+ const currentFps = parseFloat ( document . getElementById ( 'fpsInput' ) . value ) || 10 ;
408+ const currentDelay = 1000 / currentFps ;
409+ gif . addFrame ( canvas , { delay : currentDelay } ) ;
410+
411+ currentVal += step ;
412+ // Schedule next frame
413+ setTimeout ( captureFrame , 0 ) ;
414+ } catch ( err ) {
415+ console . error ( err ) ;
416+ alert ( 'Error capturing frame' ) ;
417+ if ( document . getElementById ( 'gif-progress' ) ) {
418+ document . body . removeChild ( loadingDiv ) ;
419+ }
420+ }
421+ } ;
422+
423+ captureFrame ( ) ;
424+ }
425+ } ) ;
426+
427+ // Add GIF recorder control to map
428+ map . addControl ( new L . Control . GIFRecorder ( ) ) ;
429+
240430// Custom Canvas layer for edges
241431L . CanvasEdges = L . Layer . extend ( {
242432 initialize : function ( edges , options ) {
@@ -578,8 +768,8 @@ loadDataBtn.addEventListener('click', async function() {
578768 loadDataBtn . disabled = true ;
579769
580770 // Fetch CSV files from the data subdirectory
581- const edgesUrl = `${ dirName } /edges.csv` ;
582- const densitiesUrl = `${ dirName } /densities.csv` ;
771+ const edgesUrl = `../ ${ dirName } /edges.csv` ;
772+ const densitiesUrl = `../ ${ dirName } /densities.csv` ;
583773
584774 // Load CSV data
585775 Promise . all ( [
@@ -679,7 +869,7 @@ loadDataBtn.addEventListener('click', async function() {
679869 playBtn . textContent = isPlaying ? '⏸' : '▶' ;
680870
681871 if ( isPlaying ) {
682- const fps = parseInt ( fpsInput . value ) || 10 ;
872+ const fps = parseFloat ( fpsInput . value ) || 10 ;
683873 const interval = 1000 / fps ;
684874
685875 playInterval = setInterval ( ( ) => {
@@ -850,6 +1040,9 @@ loadDataBtn.addEventListener('click', async function() {
8501040 // Hide data selector and show slider and search
8511041 document . querySelector ( '.data-selector' ) . style . display = 'none' ;
8521042 document . querySelector ( '.slider-container' ) . style . display = 'block' ;
1043+
1044+ const legendContainer = document . querySelector ( '.legend-container' ) ;
1045+ legendContainer . style . display = 'block' ;
8531046 } ) . catch ( error => {
8541047 console . error ( "Error loading CSV files:" , error ) ;
8551048 alert ( 'Error loading data files. Please check the console for details.' ) ;
0 commit comments