Skip to content

Commit e4353af

Browse files
authored
fix: restore Scheduled Task (LMFS) functionality (#248)
1 parent 29d4594 commit e4353af

6 files changed

Lines changed: 215 additions & 9 deletions

File tree

src/App.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,19 @@ class App extends React.Component {
699699
const tripLogs = new TripLogs(data.rawLogs, data.solutionType);
700700
const newVisibleToggles = getVisibleToggles(data.solutionType);
701701

702+
let newToggleOptions = { ...this.state.toggleOptions };
703+
let newExtraColumns = [...this.state.extraColumns];
704+
705+
if (data.solutionType === "LMFS") {
706+
const tasksToggleId = "showTasksAsCreated";
707+
const tasksToggle = _.find(ALL_TOGGLES, { id: tasksToggleId });
708+
if (tasksToggle && !newToggleOptions[tasksToggleId]) {
709+
log("Auto-enabling 'showTasksAsCreated' for LMFS dataset.");
710+
newToggleOptions[tasksToggleId] = true;
711+
newExtraColumns = _.union(newExtraColumns, tasksToggle.columns);
712+
}
713+
}
714+
702715
this.setState(
703716
(prevState) => ({
704717
activeDatasetIndex: index,
@@ -711,6 +724,8 @@ class App extends React.Component {
711724
},
712725
visibleToggles: newVisibleToggles,
713726
dynamicMarkerLocations: {}, // Clear markers when switching datasets
727+
toggleOptions: newToggleOptions,
728+
extraColumns: newExtraColumns,
714729
}),
715730
() => {
716731
log(`Switched to dataset ${index}`);

src/Dataframe.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function Dataframe({ featuredObject, extraColumns, onColumnToggle, onToggleMarke
159159
if (depth === 2 && indexOrName === "request") {
160160
return false;
161161
}
162-
if (depth === 3 && ["vehicle", "trip"].includes(indexOrName)) {
162+
if (depth === 3 && ["vehicle", "trip", "deliveryvehicle", "task"].includes(indexOrName)) {
163163
return false;
164164
}
165165
return true;

src/Map.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,10 @@ function MapComponent({
298298

299299
if (!map || !selectedRow) return;
300300

301-
const location = _.get(selectedRow.lastlocation, "location") || _.get(selectedRow.lastlocationResponse, "location");
301+
const location =
302+
_.get(selectedRow.lastlocation, "location") ||
303+
_.get(selectedRow.lastlocation, "rawlocation") ||
304+
_.get(selectedRow.lastlocationResponse, "location");
302305

303306
if (location?.latitude && location?.longitude) {
304307
const pos = { lat: location.latitude, lng: location.longitude };
@@ -380,8 +383,9 @@ function MapComponent({
380383
trafficLayerRef,
381384
locationProviderRef,
382385
jwt,
386+
focusSelectedRow,
383387
});
384-
}, [mapRef.current, tripLogs, taskLogs, minDate, maxDate, jwt, setFeaturedObject, setTimeRange]);
388+
}, [mapRef.current, tripLogs, taskLogs, minDate, maxDate, jwt, setFeaturedObject, setTimeRange, focusSelectedRow]);
385389

386390
useEffect(() => {
387391
if (_.isEmpty(toggleHandlers)) {

src/MapToggles.js

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export function getToggleHandlers({
124124
trafficLayerRef,
125125
locationProviderRef,
126126
jwt,
127+
focusSelectedRow,
127128
}) {
128129
const GenerateBubbles = (bubbleName, cb) => (showBubble) => {
129130
_.forEach(bubbleMapRef.current[bubbleName], (bubble) => bubble.setMap(null));
@@ -422,12 +423,105 @@ export function getToggleHandlers({
422423
const bubbleName = "showTasksAsCreated";
423424
_.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
424425
delete bubbleMapRef.current[bubbleName];
426+
427+
const getIcon = (task) => {
428+
const outcome = task.taskoutcome || "unknown";
429+
const urlBase = "http://maps.google.com/mapfiles/kml/shapes/";
430+
const icon = {
431+
url: urlBase,
432+
scaledSize: new window.google.maps.Size(30, 30),
433+
};
434+
if (outcome.includes("SUCCEEDED")) {
435+
icon.url += "flag.png";
436+
} else if (outcome.includes("FAIL")) {
437+
icon.url += "caution.png";
438+
} else {
439+
icon.url += "shaded_dot.png";
440+
}
441+
return icon;
442+
};
443+
425444
if (enabled) {
426445
log(`Enabling ${bubbleName}`);
427-
bubbleMapRef.current[bubbleName] = _.map(
428-
taskLogs.getTasks(maxDate).value(),
429-
(t) => new window.google.maps.Marker({ map, position: t.plannedlocation.point })
430-
);
446+
const tasks = taskLogs.getTasks(maxDate).value();
447+
448+
bubbleMapRef.current[bubbleName] = _(tasks)
449+
.map((task) => {
450+
if (!task.plannedlocation?.point) {
451+
return null;
452+
}
453+
454+
const marker = new window.google.maps.Marker({
455+
position: {
456+
lat: task.plannedlocation.point.latitude,
457+
lng: task.plannedlocation.point.longitude,
458+
},
459+
map: map,
460+
icon: getIcon(task),
461+
title: `${task.state}: ${task.taskid} - ${task.trackingid}`,
462+
});
463+
464+
marker.addListener("click", () => {
465+
log(`Task marker clicked: ${task.taskid}`);
466+
const latestUpdateLog = _.findLast(tripLogs.rawLogs, (le) => {
467+
const type = le["@type"];
468+
if (type === "createTask" || type === "updateTask" || type === "getTask") {
469+
const idInResp = _.get(le, "response.name", "").split("/").pop();
470+
if (idInResp === task.taskid) return true;
471+
472+
const idInReq = _.get(le, "request.taskid");
473+
if (idInReq === task.taskid) return true;
474+
}
475+
return false;
476+
});
477+
478+
if (latestUpdateLog) {
479+
log(`Found matching log entry for task ${task.taskid}`, latestUpdateLog);
480+
setFeaturedObject(latestUpdateLog);
481+
setTimeout(() => focusSelectedRow(), 0);
482+
} else {
483+
setFeaturedObject(task);
484+
}
485+
});
486+
487+
const ret = [marker];
488+
if (task.taskoutcomelocation?.point && task.plannedlocation?.point) {
489+
log(`Task ${task.taskid} has taskoutcomelocation, drawing arrow.`);
490+
const arrowColor = task.plannedVsActualDeltaMeters > 50 ? "#FF1111" : "#11FF11";
491+
const offSetPath = new window.google.maps.Polyline({
492+
path: [
493+
{
494+
lat: task.plannedlocation.point.latitude,
495+
lng: task.plannedlocation.point.longitude,
496+
},
497+
{
498+
lat: task.taskoutcomelocation.point.latitude,
499+
lng: task.taskoutcomelocation.point.longitude,
500+
},
501+
],
502+
geodesic: true,
503+
strokeColor: arrowColor,
504+
strokeOpacity: 0.6,
505+
strokeWeight: 4,
506+
map: map,
507+
icons: [
508+
{
509+
icon: {
510+
path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
511+
strokeColor: arrowColor,
512+
strokeWeight: 4,
513+
},
514+
offset: "100%",
515+
},
516+
],
517+
});
518+
ret.push(offSetPath);
519+
}
520+
return ret;
521+
})
522+
.flatten()
523+
.compact()
524+
.value();
431525
}
432526
},
433527
showPlannedPaths: (enabled) => {

src/TripLogs.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ function processRawLogs(rawLogs, solutionType) {
167167
// Create lastlocation object using deep cloned data where available
168168
newLog.lastlocation = currentLocation ? _.cloneDeep(currentLocation) : {};
169169

170+
// Data Normalization within a single log, create location from rawlocation when absent
171+
if (!newLog.lastlocation.location && newLog.lastlocation.rawlocation) {
172+
log(`processRawLogs: Falling back to rawlocation for log at ${newLog.timestamp}`);
173+
newLog.lastlocation.location = _.cloneDeep(newLog.lastlocation.rawlocation);
174+
}
175+
170176
// Apply last known location if needed
171177
if (!newLog.lastlocation.location && lastKnownState.location) {
172178
newLog.lastlocation.location = _.cloneDeep(lastKnownState.location);
@@ -222,8 +228,10 @@ function processRawLogs(rawLogs, solutionType) {
222228
}
223229

224230
// Update lastKnownState for next iterations
225-
if (currentLocation?.location) {
226-
lastKnownState.location = _.cloneDeep(currentLocation.location);
231+
const locToStore = currentLocation?.location || currentLocation?.rawlocation;
232+
if (locToStore) {
233+
log(`processRawLogs: Storing last known location for log at ${newLog.timestamp}`);
234+
lastKnownState.location = _.cloneDeep(locToStore);
227235
lastKnownState.heading = currentLocation.heading ?? lastKnownState.heading;
228236
}
229237

src/TripLogs.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,91 @@ test("basic lmfs trip log loading", async () => {
4141
]);
4242
});
4343

44+
describe("Location Data Processing", () => {
45+
test("LMFS log populates .location from .rawlocation as a fallback", () => {
46+
const rawLocation = { latitude: 37.422, longitude: -122.084 };
47+
const mockLogs = [
48+
{
49+
timestamp: "2023-01-01T12:00:00Z",
50+
jsonpayload: {
51+
"@type": "updateDeliveryVehicle",
52+
request: {
53+
deliveryvehicle: {
54+
lastlocation: {
55+
rawlocation: rawLocation,
56+
},
57+
},
58+
},
59+
response: {},
60+
},
61+
},
62+
];
63+
64+
const tripLogs = new TripLogs(mockLogs, "LMFS");
65+
expect(tripLogs.rawLogs[0].lastlocation.location).toEqual(rawLocation);
66+
});
67+
68+
test("ODRD log with both location and rawlocation prefers location", () => {
69+
const snappedLocation = { latitude: 37.7749, longitude: -122.4194 };
70+
const rawLocation = { latitude: 37.775, longitude: -122.4195 };
71+
const mockLogs = [
72+
{
73+
timestamp: "2023-01-01T10:00:00Z",
74+
jsonpayload: {
75+
"@type": "updateVehicle",
76+
request: {
77+
vehicle: {
78+
lastlocation: {
79+
location: snappedLocation,
80+
rawlocation: rawLocation,
81+
},
82+
},
83+
},
84+
response: {},
85+
},
86+
},
87+
];
88+
89+
const tripLogs = new TripLogs(mockLogs, "ODRD");
90+
expect(tripLogs.rawLogs[0].lastlocation.location).toEqual(snappedLocation);
91+
expect(tripLogs.rawLogs[0].lastlocation.location).not.toEqual(rawLocation);
92+
});
93+
94+
test("lastKnownState propagates location from an LMFS rawlocation", () => {
95+
const rawLocation = { latitude: 37.422, longitude: -122.084 };
96+
const mockLogs = [
97+
{
98+
timestamp: "2023-01-01T12:00:00Z",
99+
jsonpayload: {
100+
"@type": "updateDeliveryVehicle",
101+
request: {
102+
deliveryvehicle: {
103+
lastlocation: {
104+
rawlocation: rawLocation,
105+
heading: 180,
106+
},
107+
},
108+
},
109+
response: {},
110+
},
111+
},
112+
{
113+
timestamp: "2023-01-01T12:01:00Z",
114+
jsonpayload: {
115+
"@type": "updateDeliveryVehicle",
116+
request: {
117+
deliveryvehicle: {},
118+
},
119+
response: {},
120+
},
121+
},
122+
];
123+
const tripLogs = new TripLogs(mockLogs, "LMFS");
124+
expect(tripLogs.rawLogs[1].lastlocation.location).toEqual(rawLocation);
125+
expect(tripLogs.rawLogs[1].lastlocation.heading).toBe(180);
126+
});
127+
});
128+
44129
test("lastKnownState location is correctly applied to subsequent logs", () => {
45130
// Create mock data with two log entries
46131
const mockLogs = [

0 commit comments

Comments
 (0)