Skip to content

Commit

Permalink
feat: add LMFS task visualizations (googlemaps#132)
Browse files Browse the repository at this point in the history
* feat: add LMFS task visualizations

Shows planned location vs actual location for all tasks.  Time-sider
controls only the status of the task, not it's presence on the map.

* fix: add basic tests for LMFS tasks

Created new test and updated the lmfs demo data to a dataset
I recently generated
  • Loading branch information
greghutch authored Jun 28, 2022
1 parent df9cc36 commit c977899
Show file tree
Hide file tree
Showing 12 changed files with 395 additions and 21 deletions.
52 changes: 35 additions & 17 deletions components/bigqueryDS.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,12 @@ LIMIT
}

/*
* query update task to find tasks assigned to this vehicle and
* then use that result as subquery to find full list of create and
* update task requests associated with the specified vehicle.
* Fetch all updates to any task that was ever assigned to this vehicle. Tasks
* are enumarated by looking at stops in update vehicle and labels on update task.
*
* This should ensure logs that cover situations like a given task being
* assigned to a vehicle ... but never being updated (or being updated and assigned
* to a different vehicle).
*
* Note labels.delivery_vehicle_id is not necessarily set at creation
* time (or even on all update requests).
Expand All @@ -179,36 +182,51 @@ LIMIT
endTime: this.endTime,
};

const taskIDQuery = `
WITH
taskids AS(
SELECT
DISTINCT tasks.taskid AS taskid
FROM (
SELECT
labels.delivery_vehicle_id,
timestamp,
segments
FROM
\`${this.tablePrefix}update_delivery_vehicle\`,
UNNEST( jsonpayload_v1_updatedeliveryvehiclelog.request.deliveryvehicle.remainingvehiclejourneysegments ) segments ),
UNNEST(segments.stop.tasks) AS tasks
WHERE
delivery_vehicle_id = @delivery_vehicle_id AND ${this.timeClause}
UNION DISTINCT
SELECT
labels.task_id AS taskid
FROM
\`${this.tablePrefix}update_task\`
WHERE
labels.delivery_vehicle_id = @delivery_vehicle_id AND ${this.timeClause})
`;

const createQuery = `
${taskIDQuery}
SELECT
*
FROM
\`${this.tablePrefix}create_task\`
WHERE
${this.timeClause} and labels.task_id IN (
SELECT
DISTINCT labels.task_id
FROM
\`${this.tablePrefix}update_task\`
WHERE
labels.delivery_vehicle_id = @delivery_vehicle_id and ${this.timeClause})
${this.timeClause} and labels.task_id IN (SELECT * from taskids)
LIMIT 5000
`;

const createData = await this.bq(createQuery, params);
const updateQuery = `
${taskIDQuery}
SELECT
*
FROM
\`${this.tablePrefix}update_task\`
WHERE
${this.timeClause} and labels.task_id IN (
SELECT
DISTINCT labels.task_id
FROM
\`${this.tablePrefix}update_task\`
WHERE
labels.delivery_vehicle_id = @delivery_vehicle_id and ${this.timeClause})
${this.timeClause} and labels.task_id IN (SELECT * from taskids)
LIMIT 5000
`;

Expand Down
2 changes: 1 addition & 1 deletion datasets/lmfs.json

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions docs/Tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# LMFS Tasks

Displays all tasks assigned to the vehicle. Time slider allows replaying task state/outcome
changes.

The red or green arrows indicate the delta between the planned location and outcome location
for the task.

Task markers are clickable and will show basic data in the json viewer.

![Screenshot](screenshots/tasks.png)
Binary file added docs/screenshots/tasks.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class App extends React.Component {
showETADeltas: getToggleDefault("showETADeltas", false),
showHighVelocityJumps: getToggleDefault("showHighVelocityJumps", false),
showMissingUpdates: getToggleDefault("showMissingUpdates", false),
showTasksAsCreated: getToggleDefault("showTasksAsCreated", false),
showLiveJS: getToggleDefault("showLiveJS", false),
showClientServerTimeDeltas: getToggleDefault(
"showClientServerTimeDeltas",
Expand Down Expand Up @@ -114,6 +115,14 @@ class App extends React.Component {
columns: [],
solutionTypes: ["ODRD", "LMFS"],
},
{
id: "showTasksAsCreated",
name: "Tasks",
docLink:
"https://github.com/googlemaps/fleet-debugger/blob/main/docs/Tasks.md",
columns: [],
solutionTypes: ["LMFS"],
},
{
id: "showDwellLocations",
name: "Dwell Locations",
Expand Down
81 changes: 81 additions & 0 deletions src/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let projectId;
let locationProvider;
let solutionType;
let tripLogs;
let taskLogs;
let setFeaturedObject;
let setTimeRange;

Expand Down Expand Up @@ -286,6 +287,7 @@ function getColor(tripIdx) {

function Map(props) {
tripLogs = props.logData.tripLogs;
taskLogs = props.logData.taskLogs;
minDate = tripLogs.minDate;
maxDate = tripLogs.maxDate;
const urlParams = new URLSearchParams(window.location.search);
Expand Down Expand Up @@ -643,6 +645,85 @@ toggleHandlers["showDwellLocations"] = function (enabled) {
}
};

/*
* Draws markers on the map for all tasks.
*/
toggleHandlers["showTasksAsCreated"] = function (enabled) {
const bubbleName = "showTasksAsCreated";
const tasks = taskLogs.getTasks(maxDate).value();
_.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
delete bubbleMap[bubbleName];
function getIcon(task) {
const outcome = task.taskoutcome || "unknown";
const urlBase = "http://maps.google.com/mapfiles/kml/shapes/";
const icon = {
url: urlBase,
scaledSize: new google.maps.Size(35, 35),
};
if (outcome.match("SUCCEEDED")) {
icon.url += "flag.png";
} else if (outcome.match("FAIL")) {
icon.url += "caution.png";
} else {
icon.url += "shaded_dot.png";
}
return icon;
}
if (enabled) {
bubbleMap[bubbleName] = _(tasks)
.map((task) => {
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}`,
});
google.maps.event.addListener(marker, "click", () => {
setFeaturedObject(task);
});
const ret = [marker];
const arrowColor =
task.plannedVsActualDeltaMeters > 50 ? "#FF1111" : "#11FF11";
if (task.taskoutcomelocation) {
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: google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
strokeColor: arrowColor,
strokeWeight: 4,
},
offset: "100%",
},
],
});
ret.push(offSetPath);
}
return ret;
})
.flatten()
.value();
}
};

/*
* Draws circles on the map. Size indicates dwell time at that
* location.
Expand Down
73 changes: 73 additions & 0 deletions src/Task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Task.js
*
* Processed log for a task. Handles computing the state of
* a task at a specified time.
*/
import _ from "lodash";

class Task {
constructor(date, taskIdx, taskReq, taskResp) {
this.taskIdx = taskIdx;
this.taskId = taskReq.taskid;
this.updates = [];
this.firstUpdate = date;
this.addUpdate(date, taskReq, taskReq, taskResp);
}

/**
* Returns the status of the task at the specified date. Note that
* many task changes are actually done as side-effects of vehicle changes
* and thus the debugger only has visibily into a task change if there
* is an update_task call made.
*/
getTaskInfo(maxDate) {
const taskInfo = {
taskid: this.taskId,
};
const lastUpdate = _(this.updates)
.filter((update) => update.date <= maxDate)
.last();

if (lastUpdate) {
// The create vs update task input and output protos are annoyingly
// different. The following code attemps to handle both.
taskInfo.type = lastUpdate.taskResp.type || lastUpdate.taskReq.task.type;
taskInfo.plannedlocation =
lastUpdate.taskResp.plannedlocation ||
lastUpdate.taskReq.task.plannedlocation;
taskInfo.taskoutcome = lastUpdate.taskResp.taskoutcome;
taskInfo.state = lastUpdate.taskResp.state;
taskInfo.taskoutcomelocationsource =
lastUpdate.taskResp.taskoutcomelocationsource;
taskInfo.taskoutcomelocation = lastUpdate.taskResp.taskoutcomelocation;
taskInfo.taskoutcometime = lastUpdate.taskResp.taskoutcometime;
taskInfo.trackingid =
lastUpdate.taskResp.trackingid || lastUpdate.taskReq.task.trackingid;
if (taskInfo.taskoutcomelocationsource) {
taskInfo.plannedVsActualDeltaMeters =
window.google.maps.geometry.spherical.computeDistanceBetween(
{
lat: taskInfo.plannedlocation.point.latitude,
lng: taskInfo.plannedlocation.point.longitude,
},
{
lat: taskInfo.taskoutcomelocation.point.latitude,
lng: taskInfo.taskoutcomelocation.point.longitude,
}
);
}
}
return taskInfo;
}

addUpdate(date, taskReq, taskResp) {
this.lastUpdate = date;
this.updates.push({
date,
taskReq,
taskResp,
});
}
}
export { Task as default };
58 changes: 58 additions & 0 deletions src/TaskLogs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* TaskLogs.js
*
* Processes raw logs into task oriented logs where a single entry
* contains all the information about a task.
*/
import _ from "lodash";
import Task from "./Task";
class TaskLogs {
constructor(tripLogs) {
this.tasks = {};
this.processTasks(tripLogs.getRawLogs_());
this.minDate = tripLogs.minDate;
this.maxDate = tripLogs.maxDate;
}

processTasks(logs) {
_(logs)
.filter(
// TODO #133: response can be empty on errors -- we should highlight those rows!!
(le) =>
(le.logname.match("create_task") ||
le.logname.match("update_task")) &&
le.jsonpayload.response
)
.forEach((le, taskIdx) => {
const taskReq = le.jsonpayload.request;
const taskResp = le.jsonpayload.response;
let task = this.tasks[taskReq.taskid];
if (!task) {
task = this.tasks[taskReq.taskid] = new Task(
le.date,
taskIdx,
taskReq,
taskResp
);
} else {
task.addUpdate(le.date, taskReq, taskResp);
}
});
}

/*
* Tasks are always shown -- no matter the state of the timeslider.
*
* The timeslider is used purely to control what task state/outcome
* is displayed.
*/
getTasks(maxDate) {
maxDate = maxDate || this.maxDate;
return _(this.tasks)
.values()
.map((task) => task.getTaskInfo(maxDate))
.compact();
}
}

export { TaskLogs as default };
Loading

0 comments on commit c977899

Please sign in to comment.