Skip to content

Commit fb11fb7

Browse files
authored
feat!: find events on timeslider or on map. follow vehicle button. (#221)
* feat:timeslider clicks select events * feat: Make map clickable and find closest event to the click. * feat!:Re-Center: Follow Vehicle
1 parent 3c64f2d commit fb11fb7

File tree

6 files changed

+353
-78
lines changed

6 files changed

+353
-78
lines changed

src/App.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,9 @@ class App extends React.Component {
239239
* Callback to updated selected log row
240240
*/
241241
onSelectionChange(selectedRow, rowIndex) {
242-
log("App - onSelectionChange called with index:", rowIndex); // Debug
243-
244242
// Save both the selected row and its index for the current dataset
245243
if (this.state.activeDatasetIndex !== null && rowIndex !== undefined) {
246244
this.setState((prevState) => {
247-
log(`Saving index ${rowIndex} for dataset ${prevState.activeDatasetIndex}`); // Debug
248245
const newSelectedIndexes = [...prevState.selectedRowIndexPerDataset];
249246
newSelectedIndexes[prevState.activeDatasetIndex] = rowIndex;
250247
return {
@@ -253,7 +250,7 @@ class App extends React.Component {
253250
};
254251
});
255252
} else {
256-
log("Unable to save index:", rowIndex, "for dataset:", this.state.activeDatasetIndex); // Debug
253+
log("Unable to save index:", rowIndex, "for dataset:", this.state.activeDatasetIndex);
257254
this.setFeaturedObject(selectedRow);
258255
}
259256
}
@@ -816,6 +813,8 @@ class App extends React.Component {
816813
curMax={this.state.timeRange.maxTime}
817814
onSliderChange={this.onSliderChangeDebounced}
818815
selectedEventTime={selectedEventTime}
816+
onRowSelect={(row, rowIndex) => this.onSelectionChange(row, rowIndex)}
817+
centerOnLocation={this.centerOnLocation}
819818
/>
820819
<ToggleBar
821820
toggles={this.toggles}

src/Map.js

Lines changed: 176 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)