diff --git a/src/App.js b/src/App.js index 748b214..4e222fc 100644 --- a/src/App.js +++ b/src/App.js @@ -699,6 +699,19 @@ class App extends React.Component { const tripLogs = new TripLogs(data.rawLogs, data.solutionType); const newVisibleToggles = getVisibleToggles(data.solutionType); + let newToggleOptions = { ...this.state.toggleOptions }; + let newExtraColumns = [...this.state.extraColumns]; + + if (data.solutionType === "LMFS") { + const tasksToggleId = "showTasksAsCreated"; + const tasksToggle = _.find(ALL_TOGGLES, { id: tasksToggleId }); + if (tasksToggle && !newToggleOptions[tasksToggleId]) { + log("Auto-enabling 'showTasksAsCreated' for LMFS dataset."); + newToggleOptions[tasksToggleId] = true; + newExtraColumns = _.union(newExtraColumns, tasksToggle.columns); + } + } + this.setState( (prevState) => ({ activeDatasetIndex: index, @@ -711,6 +724,8 @@ class App extends React.Component { }, visibleToggles: newVisibleToggles, dynamicMarkerLocations: {}, // Clear markers when switching datasets + toggleOptions: newToggleOptions, + extraColumns: newExtraColumns, }), () => { log(`Switched to dataset ${index}`); diff --git a/src/Dataframe.js b/src/Dataframe.js index 710fad4..b6f7445 100644 --- a/src/Dataframe.js +++ b/src/Dataframe.js @@ -159,7 +159,7 @@ function Dataframe({ featuredObject, extraColumns, onColumnToggle, onToggleMarke if (depth === 2 && indexOrName === "request") { return false; } - if (depth === 3 && ["vehicle", "trip"].includes(indexOrName)) { + if (depth === 3 && ["vehicle", "trip", "deliveryvehicle", "task"].includes(indexOrName)) { return false; } return true; diff --git a/src/Map.js b/src/Map.js index ef10060..8087026 100644 --- a/src/Map.js +++ b/src/Map.js @@ -298,7 +298,10 @@ function MapComponent({ if (!map || !selectedRow) return; - const location = _.get(selectedRow.lastlocation, "location") || _.get(selectedRow.lastlocationResponse, "location"); + const location = + _.get(selectedRow.lastlocation, "location") || + _.get(selectedRow.lastlocation, "rawlocation") || + _.get(selectedRow.lastlocationResponse, "location"); if (location?.latitude && location?.longitude) { const pos = { lat: location.latitude, lng: location.longitude }; @@ -380,8 +383,9 @@ function MapComponent({ trafficLayerRef, locationProviderRef, jwt, + focusSelectedRow, }); - }, [mapRef.current, tripLogs, taskLogs, minDate, maxDate, jwt, setFeaturedObject, setTimeRange]); + }, [mapRef.current, tripLogs, taskLogs, minDate, maxDate, jwt, setFeaturedObject, setTimeRange, focusSelectedRow]); useEffect(() => { if (_.isEmpty(toggleHandlers)) { diff --git a/src/MapToggles.js b/src/MapToggles.js index ae99d0a..32fba73 100644 --- a/src/MapToggles.js +++ b/src/MapToggles.js @@ -124,6 +124,7 @@ export function getToggleHandlers({ trafficLayerRef, locationProviderRef, jwt, + focusSelectedRow, }) { const GenerateBubbles = (bubbleName, cb) => (showBubble) => { _.forEach(bubbleMapRef.current[bubbleName], (bubble) => bubble.setMap(null)); @@ -422,12 +423,105 @@ export function getToggleHandlers({ const bubbleName = "showTasksAsCreated"; _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null)); delete bubbleMapRef.current[bubbleName]; + + const getIcon = (task) => { + const outcome = task.taskoutcome || "unknown"; + const urlBase = "http://maps.google.com/mapfiles/kml/shapes/"; + const icon = { + url: urlBase, + scaledSize: new window.google.maps.Size(30, 30), + }; + if (outcome.includes("SUCCEEDED")) { + icon.url += "flag.png"; + } else if (outcome.includes("FAIL")) { + icon.url += "caution.png"; + } else { + icon.url += "shaded_dot.png"; + } + return icon; + }; + if (enabled) { log(`Enabling ${bubbleName}`); - bubbleMapRef.current[bubbleName] = _.map( - taskLogs.getTasks(maxDate).value(), - (t) => new window.google.maps.Marker({ map, position: t.plannedlocation.point }) - ); + const tasks = taskLogs.getTasks(maxDate).value(); + + bubbleMapRef.current[bubbleName] = _(tasks) + .map((task) => { + if (!task.plannedlocation?.point) { + return null; + } + + const marker = new window.google.maps.Marker({ + position: { + lat: task.plannedlocation.point.latitude, + lng: task.plannedlocation.point.longitude, + }, + map: map, + icon: getIcon(task), + title: `${task.state}: ${task.taskid} - ${task.trackingid}`, + }); + + marker.addListener("click", () => { + log(`Task marker clicked: ${task.taskid}`); + const latestUpdateLog = _.findLast(tripLogs.rawLogs, (le) => { + const type = le["@type"]; + if (type === "createTask" || type === "updateTask" || type === "getTask") { + const idInResp = _.get(le, "response.name", "").split("/").pop(); + if (idInResp === task.taskid) return true; + + const idInReq = _.get(le, "request.taskid"); + if (idInReq === task.taskid) return true; + } + return false; + }); + + if (latestUpdateLog) { + log(`Found matching log entry for task ${task.taskid}`, latestUpdateLog); + setFeaturedObject(latestUpdateLog); + setTimeout(() => focusSelectedRow(), 0); + } else { + setFeaturedObject(task); + } + }); + + const ret = [marker]; + if (task.taskoutcomelocation?.point && task.plannedlocation?.point) { + log(`Task ${task.taskid} has taskoutcomelocation, drawing arrow.`); + const arrowColor = task.plannedVsActualDeltaMeters > 50 ? "#FF1111" : "#11FF11"; + const offSetPath = new window.google.maps.Polyline({ + path: [ + { + lat: task.plannedlocation.point.latitude, + lng: task.plannedlocation.point.longitude, + }, + { + lat: task.taskoutcomelocation.point.latitude, + lng: task.taskoutcomelocation.point.longitude, + }, + ], + geodesic: true, + strokeColor: arrowColor, + strokeOpacity: 0.6, + strokeWeight: 4, + map: map, + icons: [ + { + icon: { + path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW, + strokeColor: arrowColor, + strokeWeight: 4, + }, + offset: "100%", + }, + ], + }); + ret.push(offSetPath); + } + return ret; + }) + .flatten() + .compact() + .value(); } }, showPlannedPaths: (enabled) => { diff --git a/src/TripLogs.js b/src/TripLogs.js index 7fb3cf6..c862673 100644 --- a/src/TripLogs.js +++ b/src/TripLogs.js @@ -167,6 +167,12 @@ function processRawLogs(rawLogs, solutionType) { // Create lastlocation object using deep cloned data where available newLog.lastlocation = currentLocation ? _.cloneDeep(currentLocation) : {}; + // Data Normalization within a single log, create location from rawlocation when absent + if (!newLog.lastlocation.location && newLog.lastlocation.rawlocation) { + log(`processRawLogs: Falling back to rawlocation for log at ${newLog.timestamp}`); + newLog.lastlocation.location = _.cloneDeep(newLog.lastlocation.rawlocation); + } + // Apply last known location if needed if (!newLog.lastlocation.location && lastKnownState.location) { newLog.lastlocation.location = _.cloneDeep(lastKnownState.location); @@ -222,8 +228,10 @@ function processRawLogs(rawLogs, solutionType) { } // Update lastKnownState for next iterations - if (currentLocation?.location) { - lastKnownState.location = _.cloneDeep(currentLocation.location); + const locToStore = currentLocation?.location || currentLocation?.rawlocation; + if (locToStore) { + log(`processRawLogs: Storing last known location for log at ${newLog.timestamp}`); + lastKnownState.location = _.cloneDeep(locToStore); lastKnownState.heading = currentLocation.heading ?? lastKnownState.heading; } diff --git a/src/TripLogs.test.js b/src/TripLogs.test.js index 33d5f9f..f085acc 100644 --- a/src/TripLogs.test.js +++ b/src/TripLogs.test.js @@ -41,6 +41,91 @@ test("basic lmfs trip log loading", async () => { ]); }); +describe("Location Data Processing", () => { + test("LMFS log populates .location from .rawlocation as a fallback", () => { + const rawLocation = { latitude: 37.422, longitude: -122.084 }; + const mockLogs = [ + { + timestamp: "2023-01-01T12:00:00Z", + jsonpayload: { + "@type": "updateDeliveryVehicle", + request: { + deliveryvehicle: { + lastlocation: { + rawlocation: rawLocation, + }, + }, + }, + response: {}, + }, + }, + ]; + + const tripLogs = new TripLogs(mockLogs, "LMFS"); + expect(tripLogs.rawLogs[0].lastlocation.location).toEqual(rawLocation); + }); + + test("ODRD log with both location and rawlocation prefers location", () => { + const snappedLocation = { latitude: 37.7749, longitude: -122.4194 }; + const rawLocation = { latitude: 37.775, longitude: -122.4195 }; + const mockLogs = [ + { + timestamp: "2023-01-01T10:00:00Z", + jsonpayload: { + "@type": "updateVehicle", + request: { + vehicle: { + lastlocation: { + location: snappedLocation, + rawlocation: rawLocation, + }, + }, + }, + response: {}, + }, + }, + ]; + + const tripLogs = new TripLogs(mockLogs, "ODRD"); + expect(tripLogs.rawLogs[0].lastlocation.location).toEqual(snappedLocation); + expect(tripLogs.rawLogs[0].lastlocation.location).not.toEqual(rawLocation); + }); + + test("lastKnownState propagates location from an LMFS rawlocation", () => { + const rawLocation = { latitude: 37.422, longitude: -122.084 }; + const mockLogs = [ + { + timestamp: "2023-01-01T12:00:00Z", + jsonpayload: { + "@type": "updateDeliveryVehicle", + request: { + deliveryvehicle: { + lastlocation: { + rawlocation: rawLocation, + heading: 180, + }, + }, + }, + response: {}, + }, + }, + { + timestamp: "2023-01-01T12:01:00Z", + jsonpayload: { + "@type": "updateDeliveryVehicle", + request: { + deliveryvehicle: {}, + }, + response: {}, + }, + }, + ]; + const tripLogs = new TripLogs(mockLogs, "LMFS"); + expect(tripLogs.rawLogs[1].lastlocation.location).toEqual(rawLocation); + expect(tripLogs.rawLogs[1].lastlocation.heading).toBe(180); + }); +}); + test("lastKnownState location is correctly applied to subsequent logs", () => { // Create mock data with two log entries const mockLogs = [