Skip to content

Commit e0931e2

Browse files
feat!: Request/Response location toggle and performance enhancements (#270)
* feat: Response Location Toggle * perf: implement log filtering cache and optimized table rendering * feat!: enable dynamic play speed and add 0.1s interval option 96adffc
1 parent 7da2161 commit e0931e2

File tree

7 files changed

+273
-95
lines changed

7 files changed

+273
-95
lines changed

src/App.js

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ class App extends React.Component {
150150
activeMenuIndex: null,
151151
initialMapBounds: null,
152152
selectedRowIndexPerDataset: [-1, -1, -1, -1, -1],
153+
useResponseLocationPerDataset: [false, false, false, false, false],
153154
currentLogData: {
154155
...this.props.logData,
155156
taskLogs: new TaskLogs(this.props.logData.tripLogs),
@@ -223,6 +224,16 @@ class App extends React.Component {
223224
this.setState({ featuredObject: featuredObject });
224225
}
225226

227+
setUseResponseLocation = (useResponseLocation) => {
228+
if (this.state.activeDatasetIndex !== null) {
229+
this.setState((prevState) => {
230+
const newValues = [...prevState.useResponseLocationPerDataset];
231+
newValues[prevState.activeDatasetIndex] = useResponseLocation;
232+
return { useResponseLocationPerDataset: newValues };
233+
});
234+
}
235+
};
236+
226237
setFocusOnRowFunction = (func) => {
227238
this.focusOnRowFunction = func;
228239
};
@@ -261,31 +272,42 @@ class App extends React.Component {
261272
});
262273
}
263274

275+
getFilteredLogs = () => {
276+
const { currentLogData, timeRange, filters, activeDatasetIndex } = this.state;
277+
if (!currentLogData || !currentLogData.tripLogs) return [];
278+
279+
const cacheKey = `${activeDatasetIndex}-${timeRange.minTime}-${timeRange.maxTime}-${JSON.stringify(filters)}`;
280+
281+
if (this._logsCache && this._logsCache.key === cacheKey) {
282+
return this._logsCache.logs;
283+
}
284+
285+
const logs = currentLogData.tripLogs
286+
.getLogs_(new Date(timeRange.minTime), new Date(timeRange.maxTime), filters)
287+
.value();
288+
289+
this._logsCache = { key: cacheKey, logs };
290+
return logs;
291+
};
292+
264293
selectFirstRow = () => {
265294
return new Promise((resolve) => {
266-
this.setState((prevState) => {
267-
const minDate = new Date(prevState.timeRange.minTime);
268-
const maxDate = new Date(prevState.timeRange.maxTime);
269-
const logs = this.state.currentLogData.tripLogs.getLogs_(minDate, maxDate, prevState.filters).value();
270-
if (logs.length > 0) {
271-
const firstRow = logs[0];
272-
setTimeout(() => this.focusOnSelectedRow(), 0);
295+
const logs = this.getFilteredLogs();
296+
if (logs.length > 0) {
297+
const firstRow = logs[0];
298+
this.setState({ featuredObject: firstRow }, () => {
299+
this.focusOnSelectedRow();
273300
resolve(firstRow);
274-
return { featuredObject: firstRow };
275-
} else {
276-
console.log("selectFirstRow: No logs found in the current time range");
277-
resolve(null);
278-
return null;
279-
}
280-
});
301+
});
302+
} else {
303+
console.log("selectFirstRow: No logs found in the current time range");
304+
resolve(null);
305+
}
281306
});
282307
};
283308

284309
selectLastRow = () => {
285-
const minDate = new Date(this.state.timeRange.minTime);
286-
const maxDate = new Date(this.state.timeRange.maxTime);
287-
const logsWrapper = this.state.currentLogData.tripLogs.getLogs_(minDate, maxDate, this.state.filters);
288-
const logs = logsWrapper.value();
310+
const logs = this.getFilteredLogs();
289311
if (logs.length > 0) {
290312
const lastRow = logs[logs.length - 1];
291313
this.setFeaturedObject(lastRow);
@@ -296,10 +318,8 @@ class App extends React.Component {
296318
};
297319

298320
handleRowChange = async (direction) => {
299-
const { featuredObject, filters } = this.state;
300-
const minDate = new Date(this.state.timeRange.minTime);
301-
const maxDate = new Date(this.state.timeRange.maxTime);
302-
const logs = this.state.currentLogData.tripLogs.getLogs_(minDate, maxDate, filters).value();
321+
const { featuredObject } = this.state;
322+
const logs = this.getFilteredLogs();
303323
let newFeaturedObject = featuredObject;
304324
const currentIndex = logs.findIndex((log) => log.timestamp === featuredObject.timestamp);
305325

@@ -345,7 +365,14 @@ class App extends React.Component {
345365

346366
handleSpeedChange = (event) => {
347367
const newSpeed = parseInt(event.target.value);
348-
this.setState({ playSpeed: newSpeed });
368+
this.setState({ playSpeed: newSpeed }, () => {
369+
if (this.state.isPlaying) {
370+
clearInterval(this.timerID);
371+
this.timerID = setInterval(() => {
372+
this.handleNextEvent();
373+
}, newSpeed);
374+
}
375+
});
349376
};
350377

351378
handleKeyPress = (event) => {
@@ -909,9 +936,7 @@ class App extends React.Component {
909936

910937
setTimeout(() => {
911938
if (savedRowIndex >= 0) {
912-
const minDate = new Date(this.state.timeRange.minTime);
913-
const maxDate = new Date(this.state.timeRange.maxTime);
914-
const logs = tripLogs.getLogs_(minDate, maxDate).value();
939+
const logs = this.getFilteredLogs();
915940

916941
if (savedRowIndex < logs.length) {
917942
log(`Restoring row at index ${savedRowIndex}`);
@@ -1014,6 +1039,12 @@ class App extends React.Component {
10141039
focusSelectedRow={this.focusOnSelectedRow}
10151040
initialMapBounds={this.state.initialMapBounds}
10161041
filters={filters}
1042+
useResponseLocation={
1043+
this.state.activeDatasetIndex !== null
1044+
? this.state.useResponseLocationPerDataset[this.state.activeDatasetIndex]
1045+
: false
1046+
}
1047+
setUseResponseLocation={this.setUseResponseLocation}
10171048
/>
10181049
</div>
10191050
<TimeSlider
@@ -1053,11 +1084,8 @@ class App extends React.Component {
10531084
</div>
10541085
<div>
10551086
<button onClick={this.handlePlayStop}>{this.state.isPlaying ? "Stop" : "Play"}</button>
1056-
<select
1057-
value={this.state.playSpeed}
1058-
onChange={this.handleSpeedChange}
1059-
disabled={this.state.isPlaying}
1060-
>
1087+
<select value={this.state.playSpeed} onChange={this.handleSpeedChange}>
1088+
<option value="100">0.1s</option>
10611089
<option value="250">0.25s</option>
10621090
<option value="500">0.5s</option>
10631091
<option value="1000">1s</option>

src/LogTable.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ function LogTable(props) {
165165
const [selectedRowIndex, setSelectedRowIndex] = useState(-1);
166166
const minTime = props.timeRange.minTime;
167167
const maxTime = props.timeRange.maxTime;
168-
const data = props.logData.tripLogs.getLogs_(new Date(minTime), new Date(maxTime), props.filters).value();
168+
const data = React.useMemo(() => {
169+
return props.logData.tripLogs.getLogs_(new Date(minTime), new Date(maxTime), props.filters).value();
170+
}, [props.logData.tripLogs, minTime, maxTime, props.filters]);
169171
const columnShortWidth = 50;
170172
const columnRegularWidth = 120;
171173
const columnLargeWidth = 150;

src/Map.js

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ function MapComponent({
2424
setCenterOnLocation,
2525
setRenderMarkerOnMap,
2626
filters,
27+
useResponseLocation,
28+
setUseResponseLocation,
2729
}) {
2830
const { tripLogs, taskLogs, jwt, projectId, mapId } = logData;
2931

@@ -112,6 +114,7 @@ function MapComponent({
112114
mapRef.current = map;
113115

114116
const tripObjects = new TripObjects({ map, setFeaturedObject, setTimeRange });
117+
mapDivRef.current.tripObjects = tripObjects;
115118

116119
const addTripPolys = () => {
117120
const trips = tripLogs.getTrips();
@@ -163,15 +166,52 @@ function MapComponent({
163166
};
164167
map.controls[window.google.maps.ControlPosition.TOP_LEFT].push(polylineButton);
165168

169+
const bottomControlsWrapper = document.createElement("div");
170+
bottomControlsWrapper.className = "map-controls-bottom-left";
171+
166172
const followButton = document.createElement("div");
167173
followButton.className = "follow-vehicle-button";
168174
followButton.innerHTML = `<div class="follow-vehicle-background"></div><svg xmlns="http://www.w3.org/2000/svg" viewBox="-10 -10 20 20" width="24" height="24" class="follow-vehicle-chevron"><path d="M -10,10 L 0,-10 L 10,10 L 0,5 z" fill="#4285F4" stroke="#4285F4" stroke-width="1"/></svg>`;
169175
followButton.onclick = () => {
170176
log("Follow vehicle button clicked.");
171177
recenterOnVehicleWrapper();
172-
map.setZoom(17);
173178
};
174-
map.controls[window.google.maps.ControlPosition.LEFT_BOTTOM].push(followButton);
179+
180+
const toggleContainer = document.createElement("div");
181+
toggleContainer.className = "map-toggle-container";
182+
183+
const updateToggleStyles = (reqActive) => {
184+
reqBtn.className = reqActive ? "map-toggle-button active" : "map-toggle-button";
185+
resBtn.className = reqActive ? "map-toggle-button" : "map-toggle-button active";
186+
};
187+
188+
const reqBtn = document.createElement("button");
189+
reqBtn.textContent = "Request";
190+
reqBtn.className = "map-toggle-button";
191+
reqBtn.onclick = () => {
192+
setUseResponseLocation(false);
193+
updateToggleStyles(true);
194+
};
195+
196+
const resBtn = document.createElement("button");
197+
resBtn.textContent = "Response";
198+
resBtn.className = "map-toggle-button";
199+
resBtn.onclick = () => {
200+
setUseResponseLocation(true);
201+
updateToggleStyles(false);
202+
};
203+
204+
const separator = document.createElement("div");
205+
separator.className = "map-toggle-separator";
206+
207+
updateToggleStyles(!useResponseLocation);
208+
209+
toggleContainer.appendChild(reqBtn);
210+
toggleContainer.appendChild(resBtn);
211+
212+
bottomControlsWrapper.appendChild(followButton);
213+
bottomControlsWrapper.appendChild(toggleContainer);
214+
map.controls[window.google.maps.ControlPosition.LEFT_BOTTOM].push(bottomControlsWrapper);
175215

176216
const centerListener = map.addListener(
177217
"center_changed",
@@ -245,16 +285,17 @@ function MapComponent({
245285
if (!map) return;
246286
log("recenterOnVehicleWrapper called for follow mode.");
247287

248-
let position = null;
249-
if (selectedRow?.lastlocation?.rawlocation) {
250-
position = selectedRow.lastlocation.rawlocation;
251-
} else if (lastValidPositionRef.current) {
252-
position = lastValidPositionRef.current;
288+
if (!isFollowingVehicle) {
289+
const locationObj = useResponseLocation ? selectedRow?.lastlocationResponse : selectedRow?.lastlocation;
290+
const position = locationObj?.location || locationObj?.rawlocation || lastValidPositionRef.current;
291+
if (position) {
292+
map.setCenter({ lat: position.latitude, lng: position.longitude });
293+
map.setZoom(17);
294+
}
253295
}
254296

255-
if (position) map.setCenter({ lat: position.latitude, lng: position.longitude });
256297
setIsFollowingVehicle((prev) => !prev);
257-
}, [selectedRow]);
298+
}, [selectedRow, useResponseLocation, isFollowingVehicle]);
258299

259300
useEffect(() => {
260301
const followButton = document.querySelector(".follow-vehicle-button");
@@ -338,16 +379,13 @@ function MapComponent({
338379
return;
339380
}
340381

341-
const location =
342-
_.get(selectedRow.lastlocation, "location") ||
343-
_.get(selectedRow.lastlocation, "rawlocation") ||
344-
_.get(selectedRow.lastlocationResponse, "location");
382+
const locationObj = useResponseLocation ? selectedRow.lastlocationResponse : selectedRow.lastlocation;
383+
const location = _.get(locationObj, "location") || _.get(locationObj, "rawlocation");
345384

346385
if (location?.latitude && location?.longitude) {
347386
const pos = { lat: location.latitude, lng: location.longitude };
348387
lastValidPositionRef.current = pos;
349-
const heading =
350-
_.get(selectedRow.lastlocation, "heading") || _.get(selectedRow.lastlocationResponse, "heading") || 0;
388+
const heading = _.get(locationObj, "heading") || 0;
351389

352390
if (vehicleMarkersRef.current.background) {
353391
vehicleMarkersRef.current.background.setPosition(pos);
@@ -396,7 +434,7 @@ function MapComponent({
396434
});
397435
}
398436

399-
const rawLocation = _.get(selectedRow.lastlocation, "rawlocation");
437+
const rawLocation = _.get(locationObj, "rawlocation");
400438
if (rawLocation?.latitude && rawLocation?.longitude) {
401439
const rawPos = { lat: rawLocation.latitude, lng: rawLocation.longitude };
402440
if (vehicleMarkersRef.current.rawLocation) {
@@ -429,7 +467,37 @@ function MapComponent({
429467
} else {
430468
Object.values(vehicleMarkersRef.current).forEach((marker) => marker && marker.setMap(null));
431469
}
432-
}, [selectedRow, isFollowingVehicle]);
470+
}, [selectedRow, isFollowingVehicle, useResponseLocation]);
471+
472+
// Update trip objects when toggle changes
473+
useEffect(() => {
474+
if (mapDivRef.current && mapDivRef.current.tripObjects) {
475+
log(`Updating TripObjects useResponseLocation to ${useResponseLocation}`);
476+
const tripObjects = mapDivRef.current.tripObjects;
477+
tripObjects.setUseResponseLocation(useResponseLocation);
478+
479+
// Redraw trips
480+
const trips = tripLogs.getTrips();
481+
_.forEach(trips, (trip) => {
482+
tripObjects.addTripVisuals(trip, minDate, maxDate);
483+
});
484+
}
485+
}, [useResponseLocation, tripLogs, minDate, maxDate]);
486+
487+
// Update toggle button UI
488+
useEffect(() => {
489+
const container = document.querySelector(".map-toggle-container");
490+
if (container) {
491+
const [reqBtn, , resBtn] = container.children;
492+
if (reqBtn && resBtn) {
493+
reqBtn.className = `map-toggle-button${!useResponseLocation ? " active" : ""}`;
494+
resBtn.className = `map-toggle-button${useResponseLocation ? " active" : ""}`;
495+
}
496+
}
497+
if (isFollowingVehicle && selectedRow) {
498+
// Re-center if we are following and the toggle changed
499+
}
500+
}, [useResponseLocation, isFollowingVehicle, selectedRow, recenterOnVehicleWrapper]);
433501

434502
const toggleHandlers = useMemo(() => {
435503
const map = mapRef.current;

src/Trip.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class Trip {
99
this.tripName = tripName;
1010
this.updateRequests = 1;
1111
this.pathCoords = [];
12+
this.pathCoordsResponse = [];
1213
this.tripDuration = 0;
1314
this.creationTime = "Unknown";
1415
this.firstUpdate = firstUpdate;
@@ -37,11 +38,12 @@ class Trip {
3738
};
3839
}
3940

40-
getPathCoords(minDate, maxDate) {
41+
getPathCoords(minDate, maxDate, useResponse = false) {
42+
const coords = useResponse ? this.pathCoordsResponse : this.pathCoords;
4143
if (!(minDate && maxDate)) {
42-
return this.pathCoords;
44+
return coords;
4345
}
44-
return _(this.pathCoords)
46+
return _(coords)
4547
.filter((le) => {
4648
return le.date >= minDate && le.date <= maxDate;
4749
})
@@ -59,6 +61,15 @@ class Trip {
5961
});
6062
}
6163

64+
appendResponseCoords(lastLocation, timestamp) {
65+
this.pathCoordsResponse.push({
66+
lat: lastLocation.location.latitude,
67+
lng: lastLocation.location.longitude,
68+
trip_id: this.tripName,
69+
date: new Date(timestamp),
70+
});
71+
}
72+
6273
setPlannedPath(plannedPath) {
6374
this.plannedPath = plannedPath.map((coords) => {
6475
return { lat: coords.latitude, lng: coords.longitude };

0 commit comments

Comments
 (0)