@@ -1,6 +1,7 @@
 
		
	
		
			
				import  React ,  {  useState ,  useEffect  }  from  'react' ;  
		
	
		
			
				import  React ,  {  useState ,  useEffect ,  useRef ,  useMemo }  from  'react' ;  
		
	
		
			
				import  *  as  leaflet  from  'leaflet' ;  
		
	
		
			
				import  {  MapContainer ,  Polyline ,  TileLayer ,  useMap  }  from  'react-leaflet ';  
		
	
		
			
				import  'leaflet-polylinedecorator ';  
		
	
		
			
				import  {  MapContainer ,  TileLayer ,  useMap ,  useMapEvents  }  from  'react-leaflet' ;  
		
	
		
			
				import  Datetime  from  'react-datetime' ;  
		
	
		
			
				import  'react-datetime/css/react-datetime.css' ;  
		
	
		
			
				import  axios  from  'axios' ;  
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -16,7 +17,7 @@ import 'react-range-slider-input/dist/style.css';
 
		
	
		
			
				const  durations  =  [  
		
	
		
			
					{ id :  0 ,  len :  'Day' ,  win :  '1m' ,  full :  '1 min' ,  delta :  [ 1 ,  'days' ] ,  format :  'HH' ,  num :  1440 ,  secs :  60 } , 
 
		
	
		
			
					{ id :  1 ,  len :  'Week' ,  win :  '3m' ,  full :  '3 min' ,  delta :  [ 7 ,  'days' ] ,  format :  'HH' ,  num :  3360 ,  secs :  180 } , 
 
		
	
		
			
					{ id :  2 ,  len :  'Month' ,  win :  '10m' ,  full :  '10 min' ,  delta :  [ 1 ,  'months' ] ,  format :  'D' ,  num :  4380 ,  secs :  600 } , 
 
		
	
		
			
					{ id :  2 ,  len :  'Month' ,  shortLen :  'Mth' ,   win :  '10m' ,  full :  '10 min' ,  delta :  [ 1 ,  'months' ] ,  format :  'D' ,  num :  4380 ,  secs :  600 } , 
 
		
	
		
			
					{ id :  3 ,  len :  'Year' ,  win :  '2h' ,  full :  '2 hour' ,  delta :  [ 1 ,  'years' ] ,  format :  'M/D' ,  num :  4380 ,  secs :  7200 } , 
 
		
	
		
			
				] ;  
		
	
		
			
				 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -38,7 +39,7 @@ const parseSlider = (end, duration, slider) => {
 
		
	
		
			
				 
		
	
		
			
				//async function sha256(source) {  
		
	
		
			
				//	const sourceBytes = new TextEncoder().encode(source);  
		
	
		
			
				//	const digest = await crypto.subtle.digest('SHA-25 6', sourceBytes);  
		
	
		
			
				//	const digest = await crypto.subtle.digest('SHA-26', sourceBytes);  
		
	
		
			
				//	const resultBytes = [...new Uint8Array(digest)];  
		
	
		
			
				//	return resultBytes.map(x => x.toString(16).padStart(2, '0')).join('');  
		
	
		
			
				//}  
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -70,14 +71,161 @@ function useSensor(measurement, name, end, duration) {
 
		
	
		
			
					return  [ data ,  loading ] ; 
 
		
	
		
			
				} ;  
		
	
		
			
				 
		
	
		
			
				function  MapViewManager ( {  coords ,  mapState ,  setMapState ,  loading ,  setSubmenu  } )  {  
		
	
		
			
				    const  map  =  useMap ( ) ; 
 
		
	
		
			
				 
		
	
		
			
				    // Effect 1: Handle map events (pan/zoom) from the user 
 
		
	
		
			
				useMapEvents ( {  
		
	
		
			
				        mousedown :  ( )  =>  setSubmenu ( false ) , 
 
		
	
		
			
				        zoomstart :  ( )  =>  setSubmenu ( false ) , 
 
		
	
		
			
				        moveend :  ( )  =>  { 
 
		
	
		
			
				            const  center  =  map . getCenter ( ) ; 
 
		
	
		
			
				            const  newZoom  =  map . getZoom ( ) ; 
 
		
	
		
			
				            const  newCenter  =  [ center . lat ,  center . lng ] ; 
 
		
	
		
			
				 
		
	
		
			
				function  Map ( { end ,  duration ,  slider } ) {  
		
	
		
			
				            setMapState ( prevState  =>   {  
		
	
		
			
				                // A small tolerance for floating point comparisons 
 
		
	
		
			
				const  tolerance  =  1e-5 ;  
		
	
		
			
				                if  ( ! prevState . center )  { 
 
		
	
		
			
				                    return  {  zoom :  newZoom ,  center :  newCenter  } ; 
 
		
	
		
			
				                } 
 
		
	
		
			
				 
		
	
		
			
				                const  zoomChanged  =  prevState . zoom  !==  newZoom ; 
 
		
	
		
			
				                const  centerChanged  =  Math . abs ( prevState . center [ 0 ]  -  newCenter [ 0 ] )  >  tolerance  || 
 
		
	
		
			
				                                    Math . abs ( prevState . center [ 1 ]  -  newCenter [ 1 ] )  >  tolerance ; 
 
		
	
		
			
				 
		
	
		
			
				                if  ( zoomChanged  ||  centerChanged )  { 
 
		
	
		
			
				                    return  {  zoom :  newZoom ,  center :  newCenter  } ; 
 
		
	
		
			
				                } 
 
		
	
		
			
				 
		
	
		
			
				                // If nothing changed, return the previous state to prevent a re-render 
 
		
	
		
			
				return  prevState ;  
		
	
		
			
				            } ) ; 
 
		
	
		
			
				        } , 
 
		
	
		
			
				    } ) ; 
 
		
	
		
			
				 
		
	
		
			
				    // Effect 2: Handle programmatic view changes (refitting or setting from state) 
 
		
	
		
			
				useEffect ( ( )  =>  {  
		
	
		
			
				        // Don't do anything while loading new data 
 
		
	
		
			
				if  ( loading )  return ;  
		
	
		
			
				 
		
	
		
			
				        // Case A: A refit is needed (signaled by null center) 
 
		
	
		
			
				if  ( mapState . center  ===  null  &&  coords . length  >  0 )  {  
		
	
		
			
				            const  bounds  =  leaflet . latLngBounds ( coords ) ; 
 
		
	
		
			
				            if  ( bounds . isValid ( ) )  { 
 
		
	
		
			
				                map . fitBounds ( bounds ) ; 
 
		
	
		
			
				                // After fitting, the 'moveend' event will fire and update the state naturally. 
 
		
	
		
			
				}  
		
	
		
			
				        } 
 
		
	
		
			
				        // Case B: A center is set in the state, ensure map is synced 
 
		
	
		
			
				else  if  ( mapState . center )  {  
		
	
		
			
				            const  currentCenter  =  map . getCenter ( ) ; 
 
		
	
		
			
				            const  currentZoom  =  map . getZoom ( ) ; 
 
		
	
		
			
				            if  ( currentCenter . lat  !==  mapState . center [ 0 ]  ||  currentCenter . lng  !==  mapState . center [ 1 ]  ||  currentZoom  !==  mapState . zoom )  { 
 
		
	
		
			
				                map . setView ( mapState . center ,  mapState . zoom ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } ,  [ coords ,  mapState ,  loading ,  map ,  setMapState ] ) ; 
 
		
	
		
			
				 
		
	
		
			
				    return  null ; 
 
		
	
		
			
				}  
		
	
		
			
				 
		
	
		
			
				function  PolylineWithArrows ( {  coords ,  showDirection  } )  {  
		
	
		
			
				    const  map  =  useMap ( ) ; 
 
		
	
		
			
				    const  polylineRef  =  useRef ( null ) ; 
 
		
	
		
			
				    const  decoratorRef  =  useRef ( null ) ; 
 
		
	
		
			
				 
		
	
		
			
				    useEffect ( ( )  =>  { 
 
		
	
		
			
				        if  ( polylineRef . current )  { 
 
		
	
		
			
				            map . removeLayer ( polylineRef . current ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				        if  ( decoratorRef . current )  { 
 
		
	
		
			
				            map . removeLayer ( decoratorRef . current ) ; 
 
		
	
		
			
				        } 
 
		
	
		
			
				 
		
	
		
			
				        if  ( coords  &&  coords . length  >  1 )  { 
 
		
	
		
			
				            const  polyline  =  leaflet . polyline ( coords ,  {  color :  'blue'  } ) ; 
 
		
	
		
			
				            polylineRef . current  =  polyline ; 
 
		
	
		
			
				            map . addLayer ( polyline ) ; 
 
		
	
		
			
				 
		
	
		
			
				            if  ( showDirection )  { 
 
		
	
		
			
				                const  decorator  =  leaflet . polylineDecorator ( polyline ,  { 
 
		
	
		
			
				                    patterns :  [ 
 
		
	
		
			
				                        { 
 
		
	
		
			
				                            offset :  25 , 
 
		
	
		
			
				                            repeat :  100 , 
 
		
	
		
			
				                            symbol :  leaflet . Symbol . arrowHead ( { 
 
		
	
		
			
				                                pixelSize :  10 , 
 
		
	
		
			
				                                polygon :  false , 
 
		
	
		
			
				                                pathOptions :  { 
 
		
	
		
			
				                                    stroke :  true , 
 
		
	
		
			
				                                    color :  'blue' , 
 
		
	
		
			
				                                    weight :  3 
 
		
	
		
			
				                                } 
 
		
	
		
			
				                            } ) 
 
		
	
		
			
				                        } 
 
		
	
		
			
				                    ] 
 
		
	
		
			
				                } ) ; 
 
		
	
		
			
				                decoratorRef . current  =  decorator ; 
 
		
	
		
			
				                map . addLayer ( decorator ) ; 
 
		
	
		
			
				            }  else  { 
 
		
	
		
			
				                decoratorRef . current  =  null ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				 
		
	
		
			
				        return  ( )  =>  { 
 
		
	
		
			
				            if  ( polylineRef . current )  { 
 
		
	
		
			
				                map . removeLayer ( polylineRef . current ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				            if  ( decoratorRef . current )  { 
 
		
	
		
			
				                map . removeLayer ( decoratorRef . current ) ; 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } ; 
 
		
	
		
			
				    } ,  [ coords ,  map ,  showDirection ] ) ; 
 
		
	
		
			
				 
		
	
		
			
				    return  null ; 
 
		
	
		
			
				}  
		
	
		
			
				 
		
	
		
			
				function  Map ( { end ,  duration ,  slider ,  mapState ,  setMapState ,  setSubmenu ,  showDirection } )  {  
		
	
		
			
					const  [ data ,  loading ]  =  useSensor ( 'owntracks' ,  'OwnTracks' ,  end ,  duration ) ; 
 
		
	
		
			
				 
		
	
		
			
					const  range  =  parseSlider ( end ,  duration ,  slider ) ; 
 
		
	
		
			
					const  range  =  useMemo ( ( )  =>  ( end ,  duration ,  slider ) ,  [ end ,  duration ,  slider ] ) 
 
		
	
		
			
				 
		
	
		
			
					const  coords  =  data . length  ?  data . filter ( x  =>  ! range  ||  ( x . time  >=  range [ 0 ]  &&  x . time  <=  range [ 1 ] ) ) . map ( ( {  lat ,  lon  } )  =>  [ lat ,  lon ] ) . filter ( ( [ lat ,  lon ] )  =>  lat  !==  null  ||  lon  !==  null )  :  [ ] ; 
 
		
	
		
			
					const  coords  =  useMemo ( ( )  =>  { 
 
		
	
		
			
						// 1. Guard against invalid top-level data 
 
		
	
		
			
				if  ( ! Array . isArray ( data ) )  {  
		
	
		
			
							return  [ ] ; 
 
		
	
		
			
						} 
 
		
	
		
			
				 
		
	
		
			
						const  result  =  [ ] ; 
 
		
	
		
			
						const  [ startTime ,  endTime ]  =  range ; 
 
		
	
		
			
				 
		
	
		
			
						// 2. Loop through the data 
 
		
	
		
			
				for  ( const  point  of  data )  {  
		
	
		
			
							// 3. Guard against malformed points 
 
		
	
		
			
				if  ( ! point  ||  typeof  point  !==  'object' )  {  
		
	
		
			
								continue ; 
 
		
	
		
			
							} 
 
		
	
		
			
				 
		
	
		
			
							const  {  lat ,  lon ,  time  }  =  point ; 
 
		
	
		
			
				 
		
	
		
			
							// 4. Guard against invalid time 
 
		
	
		
			
				if  ( typeof  time  !==  'string'  ||  time . length  ===  0 )  {  
		
	
		
			
								continue ; 
 
		
	
		
			
							} 
 
		
	
		
			
				 
		
	
		
			
							// 5. Guard against invalid coordinates (null, undefined, NaN, non-number) 
 
		
	
		
			
				if  ( typeof  lat  !==  'number'  ||  typeof  lon  !==  'number'  ||  ! isFinite ( lat )  ||  ! isFinite ( lon ) )  {  
		
	
		
			
								continue ; 
 
		
	
		
			
							} 
 
		
	
		
			
				 
		
	
		
			
							// 6. Now that all data is known to be valid, filter by time 
 
		
	
		
			
				if  ( time  >=  startTime  &&  time  <=  endTime )  {  
		
	
		
			
								result . push ( [ lat ,  lon ] ) ; 
 
		
	
		
			
							} 
 
		
	
		
			
						} 
 
		
	
		
			
						return  result ; 
 
		
	
		
			
					} ,  [ data ,  range ] ) ; 
 
		
	
		
			
				 
		
	
		
			
					const  handleSubmit  =  ( e )  =>  { 
 
		
	
		
			
						e . preventDefault ( ) ; 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -91,13 +239,16 @@ function Map({end, duration, slider}) {
 
		
	
		
			
								< p > Loading ... < / p > 
 
		
	
		
			
							: 
 
		
	
		
			
								coords . length  ? 
 
		
	
		
			
									< MapContainer  center = { coords [ coords . length - 1 ] }  zoom = { 13 }  scrollWheelZoom = { true }  style = { {  width :  '100%' ,  height :  'calc(100vh - 2.5rem)'  } } > 
 
		
	
		
			
									( 
 
		
	
		
			
										< MapContainer  center = { mapState . center  ||  [ 0 ,  0 ] }  zoom = { mapState . zoom }  scrollWheelZoom = { true }  style = { {  width :  '100%' ,  height :  'calc(100vh - 2.5rem)'  } } > 
 
		
	
		
			
											< MapViewManager  coords = { coords }  mapState = { mapState }  setMapState = { setMapState }  loading = { loading }  setSubmenu = { setSubmenu }  / > 
 
		
	
		
			
											< TileLayer 
 
		
	
		
			
												attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 
 
		
	
		
			
												url = 'https://maptiles.p.rapidapi.com/en/map/v1/{z}/{x}/{y}.png?rapidapi-key=4375b0b1d8msh0c9e7fa3efb9adfp1769dfjsnd603a0387fea' 
 
		
	
		
			
											/ > 
 
		
	
		
			
										< PolylinepathOption s = { { col or:  'blue' } }  posi tions  = { coords }  / > 
 
		
	
		
			
											 < PolylineWithArrows  coord s = { coords }  showDirec tion = { showDirection }  / > 
 
		
	
		
			
										< / M a p C o n t a i n e r > 
 
		
	
		
			
									) 
 
		
	
		
			
								: 
 
		
	
		
			
									< > 
 
		
	
		
			
										< p > No  data < / p > 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -112,8 +263,7 @@ function Map({end, duration, slider}) {
 
		
	
		
			
					) ; 
 
		
	
		
			
				}  
		
	
		
			
				 
		
	
		
			
				function  Menu ( { duration ,  setDuration ,  end ,  setEnd ,  slider ,  setSlider } )  {  
		
	
		
			
					const  [ submenu ,  setSubmenu ]  =  useState ( false ) ; 
 
		
	
		
			
				function  Menu ( { duration ,  setDuration ,  end ,  setEnd ,  slider ,  setSlider ,  submenu ,  setSubmenu ,  showDirection ,  setShowDirection )  {  
		
	
		
			
					const  [ showRange ,  setShowRange ]  =  useState ( false ) ; 
 
		
	
		
			
				 
		
	
		
			
					const  chooseDuration  =  ( x )  =>  { 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -135,12 +285,20 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
						setEnd ( moment ( ) ) ; 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					const  chooseMidnight  =  ( )  =>  { 
 
		
	
		
			
						setSubmenu ( false ) ; 
 
		
	
		
			
						setSlider ( [ 0 ,  duration . num ] ) ; 
 
		
	
		
			
						setEnd ( moment ( ) . startOf ( 'day' ) ) ; 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					const  rangeStart  =  ( x )  =>  { 
 
		
	
		
			
						setSubmenu ( false ) ; 
 
		
	
		
			
						setEnd ( moment ( range [ 0 ] ) . add ( ... duration . delta ) ) ; 
 
		
	
		
			
						setSlider ( [ 0 ,  duration . num ] ) ; 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					const  rangeEnd  =  ( x )  =>  { 
 
		
	
		
			
						setSubmenu ( false ) ; 
 
		
	
		
			
						setEnd ( moment ( range [ 1 ] ) ) ; 
 
		
	
		
			
						setSlider ( [ 0 ,  duration . num ] ) ; 
 
		
	
		
			
					} ; 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -157,6 +315,10 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
						setEnd ( prevEnd  =>  moment ( prevEnd ) . subtract ( ... duration . delta ) ) ; 
 
		
	
		
			
					} 
 
		
	
		
			
				 
		
	
		
			
					const  resetToDefaults  =  ( )  =>  { 
 
		
	
		
			
						window . location . href  =  window . location . pathname ; 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					const  range  =  parseSlider ( end ,  duration ,  slider ) ; 
 
		
	
		
			
				 
		
	
		
			
					const  rangeTime  =  ( x )  =>  { 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -167,10 +329,42 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
						} 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					const  rangeDelta  =  ( range )  =>  { 
 
		
	
		
			
						const  start  =  moment ( range [ 0 ] ) ; 
 
		
	
		
			
						const  end  =  moment ( range [ 1 ] ) ; 
 
		
	
		
			
						const  diff  =  moment . duration ( end . diff ( start ) ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  parts  =  [ ] ; 
 
		
	
		
			
				 
		
	
		
			
						const  years  =  diff . years ( ) ; 
 
		
	
		
			
						if  ( years  >  0 )  parts . push ( ` ${ years }  year ${ years  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  months  =  diff . months ( ) ; 
 
		
	
		
			
						if  ( months  >  0 )  parts . push ( ` ${ months }  month ${ months  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  days  =  diff . days ( ) ; 
 
		
	
		
			
						if  ( days  >  0 )  parts . push ( ` ${ days }  day ${ days  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  hours  =  diff . hours ( ) ; 
 
		
	
		
			
						if  ( hours  >  0 )  parts . push ( ` ${ hours }  hour ${ hours  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  minutes  =  diff . minutes ( ) ; 
 
		
	
		
			
						if  ( minutes  >  0 )  parts . push ( ` ${ minutes }  minute ${ minutes  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						const  seconds  =  diff . seconds ( ) ; 
 
		
	
		
			
						if  ( seconds  >  0 )  parts . push ( ` ${ seconds }  second ${ seconds  >  1  ?  's'  :  '' } ` ) ; 
 
		
	
		
			
				 
		
	
		
			
						if  ( parts . length  ===  0 )  { 
 
		
	
		
			
							return  '0 seconds' ; 
 
		
	
		
			
						} 
 
		
	
		
			
				 
		
	
		
			
						return  parts . join ( ', ' ) ; 
 
		
	
		
			
					} ; 
 
		
	
		
			
				 
		
	
		
			
					return  ( 
 
		
	
		
			
						< div  className = 'menu' > 
 
		
	
		
			
							{ ( showRange  ||  ! ! submenu )  &&  < div  className = 'range' > 
 
		
	
		
			
								{ rangeTime ( range [ 0 ] ) }  -  { rangeTime ( range [ 1 ] ) } 
 
		
	
		
			
								{ rangeTime ( range [ 0 ] ) }  -  { rangeTime ( range [ 1 ] ) }  < span  style = { {  whiteSpace :  'nowrap'  } } > ( { rangeDelta ( range ) } ) < / s p a n >  
 
		
	
		
			
							< / d i v > } 
 
		
	
		
			
				 
		
	
		
			
							< div  className = 'time-slider' > 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -203,9 +397,18 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
												/ > 
 
		
	
		
			
											< / d i v > 
 
		
	
		
			
				 
		
	
		
			
											< button  onClick = { chooseNow } > Jump  to  Now < / b u t t o n 
 
		
	
		
			
											< button  onClick = { rangeStart } > Shift  to  Range  Start < / b u t t o n 
 
		
	
		
			
											< button  onClick = { rangeEnd } > Shift  to  Range  End < / b u t t o n > 
 
		
	
		
			
											< div  className = 'submenu-actions' 
 
		
	
		
			
												 < div  className = 'submenu-group' 
 
		
	
		
			
													 < span > Jump  to : < / s p a n > 
 
		
	
		
			
													< button  onClick = { chooseNow } > Now < / b u t t o n > 
 
		
	
		
			
													< button  onClick = { chooseMidnight } > Midnight < / b u t t o n > 
 
		
	
		
			
												< / d i v > 
 
		
	
		
			
												< div  className = 'submenu-group' > 
 
		
	
		
			
													< span > Shift  to : < / s p a n > 
 
		
	
		
			
													< button  onClick = { rangeStart } > Range  Start < / b u t t o n > 
 
		
	
		
			
													< button  onClick = { rangeEnd } > Range  End < / b u t t o n > 
 
		
	
		
			
												< / d i v > 
 
		
	
		
			
											< / d i v > 
 
		
	
		
			
										< / > 
 
		
	
		
			
									} 
 
		
	
		
			
				 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -221,6 +424,24 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
											) } 
 
		
	
		
			
										< / > 
 
		
	
		
			
									} 
 
		
	
		
			
				 
		
	
		
			
									{ submenu  ===  'misc'  && 
 
		
	
		
			
										< > 
 
		
	
		
			
											< div  className = 'submenu-header' > 
 
		
	
		
			
												< h2 > Misc < / h 2 > 
 
		
	
		
			
												< button  onClick = { ( )  =>  setSubmenu ( false ) } > × < / b u t t o n > 
 
		
	
		
			
											< / d i v > 
 
		
	
		
			
											< label  className = "submenu-checkbox-label" > 
 
		
	
		
			
												< input 
 
		
	
		
			
													type = "checkbox" 
 
		
	
		
			
													checked = { showDirection } 
 
		
	
		
			
													onChange = { e  =>  setShowDirection ( e . target . checked ) } 
 
		
	
		
			
												/ > 
 
		
	
		
			
												Show  direction 
 
		
	
		
			
											< / l a b e l > 
 
		
	
		
			
											< button  onClick = { resetToDefaults } > Reset  to  defaults < / b u t t o n > 
 
		
	
		
			
										< / > 
 
		
	
		
			
									} 
 
		
	
		
			
								< / d i v > 
 
		
	
		
			
							} 
 
		
	
		
			
				 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -231,14 +452,21 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
									onClick = { ( )  =>  setSubmenu ( 'end' ) } 
 
		
	
		
			
									className = { submenu  ===  'end'  ?  'active'  :  '' } 
 
		
	
		
			
								> 
 
		
	
		
			
									{ moment ( end ) . subtract ( duration . delta [ 0 ] ,  duration . delta [ 1 ] ) . format ( 'ddd  MMM DD' ) } 
 
		
	
		
			
									{ moment ( end ) . subtract ( duration . delta [ 0 ] ,  duration . delta [ 1 ] ) . format ( 'dd MMM DD' ) } 
 
		
	
		
			
								< / b u t t o n > 
 
		
	
		
			
				 
		
	
		
			
								< button 
 
		
	
		
			
									onClick = { ( )  =>  setSubmenu ( 'misc' ) } 
 
		
	
		
			
									className = { submenu  ===  'misc'  ?  'active'  :  '' } 
 
		
	
		
			
								> 
 
		
	
		
			
									☰ 
 
		
	
		
			
								< / b u t t o n > 
 
		
	
		
			
				 
		
	
		
			
								< button 
 
		
	
		
			
									onClick = { ( )  =>  setSubmenu ( 'duration' ) } 
 
		
	
		
			
									className = { submenu  ===  'duration'  ?  'active'  :  '' } 
 
		
	
		
			
								> 
 
		
	
		
			
									{ duration . len }  /  { duration . win } 
 
		
	
		
			
									{ ( duration . shortLen  ||  duration . len ) /  { duration . win } 
 
		
	
		
			
								< / b u t t o n > 
 
		
	
		
			
				 
		
	
		
			
								< button  onClick = { ( )  =>  next ( ) } > & gt ; < / b u t t o n > 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -248,9 +476,60 @@ function Menu({duration, setDuration, end, setEnd, slider, setSlider}) {
 
		
	
		
			
				}  
		
	
		
			
				 
		
	
		
			
				function  App ( )  {  
		
	
		
			
					const  [ duration ,  setDuration ]  =  useState ( dur ations [ 0 ] ) ; 
 
		
	
		
			
					const  [ end ,  setEnd ]  =  useState ( moment ( ) ) ; 
 
		
	
		
			
					const  [ slider ,  setSlider ]  =  useState ( [ 0 ,  duration . num ] ) ; 
 
		
	
		
			
					const  params  =  new  URLSearchParams ( window . loc ation. search ) ; 
 
		
	
		
			
					const  initialDurationId  =  params . get ( 'duration' ) ; 
 
		
	
		
			
					const  initialEndTimestamp  =  params . get ( 'end' ) ; 
 
		
	
		
			
					const  initialSliderValue  =  params . get ( 'slider' ) ; 
 
		
	
		
			
					const  initialLat  =  params . get ( 'lat' ) ; 
 
		
	
		
			
					const  initialLng  =  params . get ( 'lng' ) ; 
 
		
	
		
			
					const  initialZoom  =  params . get ( 'zoom' ) ; 
 
		
	
		
			
					const  initialShowDirection  =  params . get ( 'showDirection' )  ===  'true' ; 
 
		
	
		
			
				 
		
	
		
			
					const  initialDuration  =  ( initialDurationId  &&  durations [ parseInt ( initialDurationId ,  10 ) ] )  ?  durations [ parseInt ( initialDurationId ,  10 ) ]  :  durations [ 0 ] ; 
 
		
	
		
			
					const  initialEnd  =  initialEndTimestamp  ?  moment . unix ( initialEndTimestamp )  :  moment ( ) ; 
 
		
	
		
			
					const  initialSlider  =  initialSliderValue  ?  initialSliderValue . split ( ',' ) . map ( Number )  :  [ 0 ,  initialDuration . num ] ; 
 
		
	
		
			
				 
		
	
		
			
					const  [ duration ,  setDuration ]  =  useState ( initialDuration ) ; 
 
		
	
		
			
					const  [ end ,  setEnd ]  =  useState ( initialEnd ) ; 
 
		
	
		
			
					const  [ slider ,  setSlider ]  =  useState ( initialSlider ) ; 
 
		
	
		
			
					const  [ mapState ,  setMapState ]  =  useState ( { 
 
		
	
		
			
						center :  ( initialLat  &&  initialLng )  ?  [ parseFloat ( initialLat ) ,  parseFloat ( initialLng ) ]  :  null , 
 
		
	
		
			
						zoom :  initialZoom  ?  parseInt ( initialZoom ,  10 )  :  13 , 
 
		
	
		
			
					} ) ; 
 
		
	
		
			
					const  [ submenu ,  setSubmenu ]  =  useState ( false ) ; 
 
		
	
		
			
					const  [ showDirection ,  setShowDirection ]  =  useState ( initialShowDirection ) ; 
 
		
	
		
			
				 
		
	
		
			
					const  isInitialMount  =  useRef ( true ) ; 
 
		
	
		
			
					useEffect ( ( )  =>  { 
 
		
	
		
			
						if  ( isInitialMount . current )  { 
 
		
	
		
			
							isInitialMount . current  =  false ; 
 
		
	
		
			
						}  else  { 
 
		
	
		
			
							// Reset map center to trigger a refit when new data arrives 
 
		
	
		
			
				setMapState ( prev  =>  ( {  ... prev ,  center :  null  } ) ) ;  
		
	
		
			
						} 
 
		
	
		
			
					} ,  [ end ,  duration ] ) ; 
 
		
	
		
			
				 
		
	
		
			
					useEffect ( ( )  =>  { 
 
		
	
		
			
						const  handler  =  setTimeout ( ( )  =>  { 
 
		
	
		
			
							const  params  =  new  URLSearchParams ( ) ; 
 
		
	
		
			
							params . set ( 'duration' ,  duration . id ) ; 
 
		
	
		
			
							params . set ( 'end' ,  end . unix ( ) ) ; 
 
		
	
		
			
							params . set ( 'slider' ,  slider . join ( ',' ) ) ; 
 
		
	
		
			
							if  ( showDirection )  { 
 
		
	
		
			
								params . set ( 'showDirection' ,  'true' ) ; 
 
		
	
		
			
							} 
 
		
	
		
			
							if  ( mapState . center )  { 
 
		
	
		
			
								params . set ( 'lat' ,  mapState . center [ 0 ] . toFixed ( 5 ) ) ; 
 
		
	
		
			
								params . set ( 'lng' ,  mapState . center [ 1 ] . toFixed ( 5 ) ) ; 
 
		
	
		
			
								params . set ( 'zoom' ,  mapState . zoom ) ; 
 
		
	
		
			
							} 
 
		
	
		
			
							window . history . replaceState ( { } ,  '' ,  ` ${ window . location . pathname } ? ${ params . toString ( ) } ` ) ; 
 
		
	
		
			
						} ,  500 ) ; 
 
		
	
		
			
				 
		
	
		
			
						return  ( )  =>  { 
 
		
	
		
			
							clearTimeout ( handler ) ; 
 
		
	
		
			
						} ; 
 
		
	
		
			
					} ,  [ duration ,  end ,  slider ,  mapState ,  showDirection ] ) ; 
 
		
	
		
			
				 
		
	
		
			
					return  ( 
 
		
	
		
			
						< div > 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -261,12 +540,20 @@ function App() {
 
		
	
		
			
								setEnd = { setEnd } 
 
		
	
		
			
								slider = { slider } 
 
		
	
		
			
								setSlider = { setSlider } 
 
		
	
		
			
								submenu = { submenu } 
 
		
	
		
			
								setSubmenu = { setSubmenu } 
 
		
	
		
			
								showDirection = { showDirection } 
 
		
	
		
			
								setShowDirection = { setShowDirection } 
 
		
	
		
			
							/ > 
 
		
	
		
			
				 
		
	
		
			
							< Map 
 
		
	
		
			
								end = { end } 
 
		
	
		
			
								duration = { duration } 
 
		
	
		
			
								slider = { slider } 
 
		
	
		
			
								mapState = { mapState } 
 
		
	
		
			
								setMapState = { setMapState } 
 
		
	
		
			
								setSubmenu = { setSubmenu } 
 
		
	
		
			
								showDirection = { showDirection } 
 
		
	
		
			
							/ > 
 
		
	
		
			
						< / d i v > 
 
		
	
		
			
					) ;