@@ -44,6 +44,19 @@ function addTripPolys(map) {
4444 const trips = tripLogs . getTrips ( ) ;
4545 const vehicleBounds = new window . google . maps . LatLngBounds ( ) ;
4646
47+ // Add click handler to map for finding nearby events
48+ map . addListener ( "click" , ( event ) => {
49+ const clickLocation = event . latLng ;
50+ log ( "Map click detected at location:" , clickLocation . lat ( ) , clickLocation . lng ( ) ) ;
51+
52+ // Find the closest event within 250 meters
53+ const closestEvent = findClosestEvent ( clickLocation , 250 ) ;
54+ if ( closestEvent ) {
55+ log ( "Found closest event:" , closestEvent . timestamp ) ;
56+ setFeaturedObject ( closestEvent ) ;
57+ }
58+ } ) ;
59+
4760 _ . forEach ( trips , ( trip ) => {
4861 tripObjects . addTripVisuals ( trip , minDate , maxDate ) ;
4962
@@ -99,6 +112,8 @@ function MyMapComponent(props) {
99112 const [ showPolylineUI , setShowPolylineUI ] = useState ( false ) ;
100113 const [ polylines , setPolylines ] = useState ( [ ] ) ;
101114 const [ buttonPosition , setButtonPosition ] = useState ( { top : 0 , left : 0 } ) ;
115+ const [ isFollowingVehicle , setIsFollowingVehicle ] = useState ( false ) ;
116+ const lastValidPositionRef = useRef ( null ) ;
102117
103118 useEffect ( ( ) => {
104119 const urlZoom = getQueryStringValue ( "zoom" ) ;
@@ -120,12 +135,19 @@ function MyMapComponent(props) {
120135 map . setOptions ( { maxZoom : 100 } ) ;
121136 map . addListener ( "zoom_changed" , ( ) => {
122137 setQueryStringValue ( "zoom" , map . getZoom ( ) ) ;
138+ setIsFollowingVehicle ( false ) ; // zoom disables following
139+ log ( "Follow mode disabled due to zoom change" ) ;
123140 } ) ;
124141
125142 map . addListener ( "heading_changed" , ( ) => {
126143 setQueryStringValue ( "heading" , map . getHeading ( ) ) ;
127144 } ) ;
128145
146+ map . addListener ( "dragstart" , ( ) => {
147+ setIsFollowingVehicle ( false ) ;
148+ log ( "Follow mode disabled due to map drag" ) ;
149+ } ) ;
150+
129151 map . addListener (
130152 "center_changed" ,
131153 _ . debounce ( ( ) => {
@@ -145,59 +167,44 @@ function MyMapComponent(props) {
145167 } ;
146168
147169 map . controls [ google . maps . ControlPosition . TOP_LEFT ] . push ( polylineButton ) ;
148- } , [ ] ) ;
149-
150- useEffect ( ( ) => {
151- if ( ! props . selectedRow ) return ;
152-
153- // Clear ALL route segment polylines
154- polylines . forEach ( ( polyline ) => {
155- if ( polyline . isRouteSegment ) {
156- polyline . setMap ( null ) ;
157- }
158- } ) ;
159- // Update the polylines state to remove all route segments
160- setPolylines ( polylines . filter ( ( p ) => ! p . isRouteSegment ) ) ;
161170
162- // Get the current route segment from the selected row
163- const routeSegment =
164- _ . get ( props . selectedRow , "request.vehicle.currentroutesegment" ) ||
165- _ . get ( props . selectedRow , "lastlocation.currentroutesegment" ) ;
166-
167- if ( routeSegment ) {
168- try {
169- const decodedPoints = decode ( routeSegment ) ;
171+ // Create follow vehicle button with chevron icon
172+ const followButton = document . createElement ( "div" ) ;
173+ followButton . className = "follow-vehicle-button" ;
174+ followButton . innerHTML = `
175+ <div class="follow-vehicle-background"></div>
176+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 20 20" width="24" height="24" class="follow-vehicle-chevron">
177+ <path d="M -10,10 L 0,-10 L 10,10 L 0,5 z" fill="#4285F4" stroke="#4285F4" stroke-width="1"/>
178+ </svg>
179+ ` ;
180+
181+ followButton . onclick = ( ) => {
182+ log ( "Follow vehicle button clicked" ) ;
183+ recenterOnVehicle ( ) ;
184+ } ;
170185
171- if ( decodedPoints && decodedPoints . length > 0 ) {
172- const validWaypoints = decodedPoints . map ( ( point ) => ( {
173- lat : point . latDegrees ( ) ,
174- lng : point . lngDegrees ( ) ,
175- } ) ) ;
186+ // Add button to map
187+ map . controls [ google . maps . ControlPosition . LEFT_BOTTOM ] . push ( followButton ) ;
176188
177- const trafficRendering =
178- _ . get ( props . selectedRow , "request.vehicle.currentroutesegmenttraffic.trafficrendering" ) ||
179- _ . get ( props . selectedRow , "lastlocation.currentroutesegmenttraffic.trafficrendering" ) ;
180-
181- const rawLocation = _ . get ( props . selectedRow . lastlocation , "rawlocation" ) ;
189+ updateFollowButtonAppearance ( ) ;
190+ } , [ ] ) ;
182191
183- const trafficPolyline = new TrafficPolyline ( {
184- path : validWaypoints ,
185- zIndex : 2 ,
186- trafficRendering : structuredClone ( trafficRendering ) ,
187- currentLatLng : rawLocation ,
188- map : map ,
189- } ) ;
190- setPolylines ( ( prev ) => [ ...prev , ...trafficPolyline . polylines ] ) ;
191- }
192- } catch ( error ) {
193- console . error ( "Error processing route segment polyline:" , {
194- error,
195- routeSegment,
196- rowData : props . selectedRow ,
197- } ) ;
192+ useEffect ( ( ) => {
193+ updateFollowButtonAppearance ( ) ;
194+ } , [ isFollowingVehicle ] ) ;
195+
196+ const updateFollowButtonAppearance = ( ) => {
197+ const followButton = document . querySelector ( ".follow-vehicle-button" ) ;
198+ if ( followButton ) {
199+ if ( isFollowingVehicle ) {
200+ followButton . classList . add ( "active" ) ;
201+ log ( "Follow vehicle button updated to active state" ) ;
202+ } else {
203+ followButton . classList . remove ( "active" ) ;
204+ log ( "Follow vehicle button updated to inactive state" ) ;
198205 }
199206 }
200- } , [ props . selectedRow , map ] ) ;
207+ } ;
201208
202209 const handlePolylineSubmit = ( waypoints , properties ) => {
203210 const path = waypoints . map ( ( wp ) => new google . maps . LatLng ( wp . latitude , wp . longitude ) ) ;
@@ -235,6 +242,40 @@ function MyMapComponent(props) {
235242 ) ;
236243 } ;
237244
245+ const recenterOnVehicle = ( ) => {
246+ log ( "Executing recenterOnVehicle function" ) ;
247+
248+ let position = null ;
249+
250+ // Try to get position from current row
251+ if ( props . selectedRow && props . selectedRow . lastlocation && props . selectedRow . lastlocation . rawlocation ) {
252+ position = props . selectedRow . lastlocation . rawlocation ;
253+ log ( `Found position in selected row: ${ position . latitude } , ${ position . longitude } ` ) ;
254+ }
255+ // Try to use our last cached valid position
256+ else if ( lastValidPositionRef . current ) {
257+ position = lastValidPositionRef . current ;
258+ log ( `Using last cached valid position: ${ position . lat } , ${ position . lng } ` ) ;
259+ }
260+ if ( ! position ) {
261+ log ( "No vehicle position found" ) ;
262+ }
263+
264+ // Center the map
265+ if ( position && typeof position . latitude !== "undefined" ) {
266+ map . setCenter ( { lat : position . latitude , lng : position . longitude } ) ;
267+ map . setZoom ( 17 ) ;
268+ }
269+
270+ setIsFollowingVehicle ( ( prev ) => {
271+ const newState = ! prev ;
272+ log ( `Map follow mode ${ newState ? "enabled" : "disabled" } ` ) ;
273+ return newState ;
274+ } ) ;
275+
276+ log ( `Map centered on vehicle, follow mode ${ ! isFollowingVehicle ? "enabled" : "disabled" } ` ) ;
277+ } ;
278+
238279 /*
239280 * Handler for timewindow change. Updates global min/max date globals
240281 * and recomputes the paths as well as all the bubble markers to respect the
@@ -266,7 +307,59 @@ function MyMapComponent(props) {
266307 } , [ props . rangeStart , props . rangeEnd ] ) ;
267308
268309 useEffect ( ( ) => {
269- // Car location maker
310+ if ( ! props . selectedRow ) return ;
311+
312+ // Clear ALL route segment polylines
313+ polylines . forEach ( ( polyline ) => {
314+ if ( polyline . isRouteSegment ) {
315+ polyline . setMap ( null ) ;
316+ }
317+ } ) ;
318+ // Update the polylines state to remove all route segments
319+ setPolylines ( polylines . filter ( ( p ) => ! p . isRouteSegment ) ) ;
320+
321+ // Get the current route segment from the selected row
322+ const routeSegment =
323+ _ . get ( props . selectedRow , "request.vehicle.currentroutesegment" ) ||
324+ _ . get ( props . selectedRow , "lastlocation.currentroutesegment" ) ;
325+
326+ if ( routeSegment ) {
327+ try {
328+ const decodedPoints = decode ( routeSegment ) ;
329+
330+ if ( decodedPoints && decodedPoints . length > 0 ) {
331+ const validWaypoints = decodedPoints . map ( ( point ) => ( {
332+ lat : point . latDegrees ( ) ,
333+ lng : point . lngDegrees ( ) ,
334+ } ) ) ;
335+
336+ const trafficRendering =
337+ _ . get ( props . selectedRow , "request.vehicle.currentroutesegmenttraffic.trafficrendering" ) ||
338+ _ . get ( props . selectedRow , "lastlocation.currentroutesegmenttraffic.trafficrendering" ) ;
339+
340+ const rawLocation = _ . get ( props . selectedRow . lastlocation , "rawlocation" ) ;
341+
342+ const trafficPolyline = new TrafficPolyline ( {
343+ path : validWaypoints ,
344+ zIndex : 2 ,
345+ trafficRendering : structuredClone ( trafficRendering ) ,
346+ currentLatLng : rawLocation ,
347+ map : map ,
348+ } ) ;
349+ setPolylines ( ( prev ) => [ ...prev , ...trafficPolyline . polylines ] ) ;
350+ }
351+ } catch ( error ) {
352+ console . error ( "Error processing route segment polyline:" , {
353+ error,
354+ routeSegment,
355+ rowData : props . selectedRow ,
356+ } ) ;
357+ }
358+ }
359+ } , [ props . selectedRow ] ) ;
360+
361+ useEffect ( ( ) => {
362+ // Vehicle chevron location maker
270363 const data = props . selectedRow ;
271364 if ( ! data ) return ;
272365 _ . forEach ( dataMakers , ( m ) => m . setMap ( null ) ) ;
@@ -295,6 +388,8 @@ function MyMapComponent(props) {
295388
296389 const rawLocation = _ . get ( data . lastlocation , "rawlocation" ) ;
297390 if ( rawLocation ) {
391+ lastValidPositionRef . current = { lat : rawLocation . latitude , lng : rawLocation . longitude } ;
392+
298393 const heading = _ . get ( data . lastlocation , "heading" ) || 0 ;
299394 markerSymbols . chevron . rotation = heading ;
300395
@@ -314,8 +409,12 @@ function MyMapComponent(props) {
314409 } ) ;
315410
316411 dataMakers . push ( backgroundMarker , chevronMarker ) ;
412+
413+ if ( isFollowingVehicle ) {
414+ map . setCenter ( { lat : rawLocation . latitude , lng : rawLocation . longitude } ) ;
415+ }
317416 }
318- } , [ props . selectedRow ] ) ;
417+ } , [ props . selectedRow , isFollowingVehicle ] ) ;
319418
320419 for ( const toggle of props . toggles ) {
321420 const id = toggle . id ;
@@ -378,6 +477,34 @@ function Map(props) {
378477 ) ;
379478}
380479
480+ // Add a new function to find the closest event to a clicked location
481+ function findClosestEvent ( clickLocation , maxDistance ) {
482+ const logs = tripLogs . getLogs_ ( minDate , maxDate ) . value ( ) ;
483+ let closestEvent = null ;
484+ let closestDistance = maxDistance ;
485+
486+ logs . forEach ( ( event ) => {
487+ const rawLocation = _ . get ( event , "lastlocation.rawlocation" ) ;
488+ if ( rawLocation && rawLocation . latitude && rawLocation . longitude ) {
489+ const eventLocation = new google . maps . LatLng ( rawLocation . latitude , rawLocation . longitude ) ;
490+ const distance = google . maps . geometry . spherical . computeDistanceBetween ( clickLocation , eventLocation ) ;
491+
492+ if ( distance < closestDistance ) {
493+ closestEvent = event ;
494+ closestDistance = distance ;
495+ }
496+ }
497+ } ) ;
498+
499+ if ( closestEvent ) {
500+ log ( "Found closest event at distance:" , closestDistance , "meters" ) ;
501+ } else {
502+ log ( "No events found within" , maxDistance , "meters" ) ;
503+ }
504+
505+ return closestEvent ;
506+ }
507+
381508/*
382509 * GenerateBubbles() -- helper function for generating map features based
383510 * on per-log entry data.
0 commit comments