From 0b50dd10fea33f28116c613be391b79d916d56e6 Mon Sep 17 00:00:00 2001 From: regeter <2320305+regeter@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:17:22 -0800 Subject: [PATCH 1/4] fix: Rename some Toggles to make it clearer --- src/App.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/App.js b/src/App.js index 6256a39..5c83a1f 100644 --- a/src/App.js +++ b/src/App.js @@ -161,14 +161,14 @@ class App extends React.Component { }, { id: "showTraffic", - name: "Traffic", + name: "Live Traffic", docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md", columns: [], solutionTypes: ["ODRD", "LMFS"], }, { id: "showLiveJS", - name: "Start Live Journey Sharing for newest trip", + name: "Live Journey Sharing", docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md", columns: [], solutionTypes: ["ODRD", "LMFS"], From 680542ed0eb884ed2e2af505b133e7a0584ff71a Mon Sep 17 00:00:00 2001 From: regeter <2320305+regeter@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:18:01 -0800 Subject: [PATCH 2/4] feat: Add TripObjects to handle polylines and markers of trips --- src/TripObjects.js | 217 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/TripObjects.js diff --git a/src/TripObjects.js b/src/TripObjects.js new file mode 100644 index 0000000..b12040a --- /dev/null +++ b/src/TripObjects.js @@ -0,0 +1,217 @@ +// src/TripObjects.js + +import { log } from "./Utils"; +import { getColor } from "./Trip"; + +export class TripObjects { + constructor({ map, setFeaturedObject, setTimeRange }) { + this.map = map; + this.markers = new Map(); + this.paths = new Map(); + this.arrows = new Map(); + this.setFeaturedObject = setFeaturedObject; + this.setTimeRange = setTimeRange; + } + + createSVGMarker(type, color) { + const isPickup = type.toLowerCase().includes("pickup"); + const isActual = type.includes("actual"); + + const createChevronPath = (direction, shift = 0) => { + let points; + if (direction === "up") { + points = [ + { x: 12, y: 20 + shift }, // Top point of chevron + { x: 8, y: 24 + shift }, // Bottom left + { x: 16, y: 24 + shift }, // Bottom right + { x: 12, y: 20 + shift }, + ]; + } else { + // "down" + points = [ + { x: 12, y: 24 + shift }, // Bottom point of chevron + { x: 8, y: 20 + shift }, // Top left + { x: 16, y: 20 + shift }, // Top right + { x: 12, y: 24 + shift }, + ]; + } + + return ``; + }; + + const svgBase = ` + + ${isActual ? (isPickup ? createChevronPath("up", -4) : createChevronPath("down", -4)) : ""} + ${isPickup ? createChevronPath("up") : createChevronPath("down")} + `; + + return { + url: "data:image/svg+xml;charset=UTF-8," + encodeURIComponent(svgBase), + scaledSize: new google.maps.Size(24, 24), + anchor: new google.maps.Point(12, 24), + }; + } + + createMarkerWithEvents(point, type, color, pairedPoint, tripId) { + if (!point) return null; + + const marker = new google.maps.Marker({ + position: { lat: point.latitude, lng: point.longitude }, + icon: this.createSVGMarker(type, color), + map: this.map, + }); + + if (pairedPoint) { + // Store the arrow in the class instance using a unique key + const arrowKey = `${tripId}_${type.includes("actual") ? type.replace("actual", "") : type}`; + + google.maps.event.addListener(marker, "click", () => { + log(`${type} marker clicked`); + + if (this.arrows.has(arrowKey)) { + this.arrows.get(arrowKey).setMap(null); + this.arrows.delete(arrowKey); + } else { + // Create arrow from requested to actual + const from = type.includes("actual") ? pairedPoint : point; + const to = type.includes("actual") ? point : pairedPoint; + const arrow = this.createConnectingArrow(from, to, color); + if (arrow) { + this.arrows.set(arrowKey, arrow); + } + } + }); + } + + return marker; + } + + createConnectingArrow(from, to, color) { + if (!from || !to) return null; + + return new google.maps.Polyline({ + path: [ + { lat: from.latitude, lng: from.longitude }, + { lat: to.latitude, lng: to.longitude }, + ], + geodesic: true, + strokeColor: color, + strokeOpacity: 1, + strokeWeight: 1, + icons: [ + { + icon: { + path: google.maps.SymbolPath.FORWARD_CLOSED_ARROW, + scale: 1, + }, + offset: "100%", + }, + ], + map: this.map, + }); + } + + addTripVisuals(trip, minDate, maxDate) { + const tripId = trip.tripName; + log(`Processing trip visuals for ${tripId}`, { + pickupPoint: trip.getPickupPoint(), + actualPickupPoint: trip.getActualPickupPoint(), + dropoffPoint: trip.getDropoffPoint(), + actualDropoffPoint: trip.getActualDropoffPoint(), + }); + + this.clearTripObjects(tripId); + + // Add path polyline + const tripCoords = trip.getPathCoords(minDate, maxDate); + if (tripCoords.length > 0) { + const path = new google.maps.Polyline({ + path: tripCoords, + geodesic: true, + strokeColor: getColor(trip.tripIdx), + strokeOpacity: 0.5, + strokeWeight: 6, + map: this.map, + }); + + // Add path events + google.maps.event.addListener(path, "mouseover", () => { + path.setOptions({ strokeOpacity: 1, strokeWeight: 8 }); + }); + + google.maps.event.addListener(path, "mouseout", () => { + path.setOptions({ strokeOpacity: 0.5, strokeWeight: 6 }); + }); + + google.maps.event.addListener(path, "click", () => { + log("Trip polyline clicked"); + const fd = trip.getFeaturedData(); + this.setFeaturedObject(fd); + // TODO: https://github.com/googlemaps/fleet-debugger/issues/79 + // this time range won't capture the createTrip logs + this.setTimeRange(fd.firstUpdate.getTime(), fd.lastUpdate.getTime()); + }); + + this.paths.set(tripId, path); + } + + const markers = []; + + // Get points + const pickupPoint = trip.getPickupPoint(); + const actualPickupPoint = trip.getActualPickupPoint(); + const dropoffPoint = trip.getDropoffPoint(); + const actualDropoffPoint = trip.getActualDropoffPoint(); + + // Create pickup markers + const pickupMarker = this.createMarkerWithEvents(pickupPoint, "pickup", "#3d633d", actualPickupPoint, tripId); + if (pickupMarker) markers.push(pickupMarker); + + const actualPickupMarker = this.createMarkerWithEvents( + actualPickupPoint, + "actualPickup", + "#3d633d", + pickupPoint, + tripId + ); + if (actualPickupMarker) markers.push(actualPickupMarker); + + // Create dropoff markers + const dropoffMarker = this.createMarkerWithEvents(dropoffPoint, "dropoff", "#0000FF", actualDropoffPoint, tripId); + if (dropoffMarker) markers.push(dropoffMarker); + + const actualDropoffMarker = this.createMarkerWithEvents( + actualDropoffPoint, + "actualDropoff", + "#0000FF", + dropoffPoint, + tripId + ); + if (actualDropoffMarker) markers.push(actualDropoffMarker); + + this.markers.set(tripId, markers); + } + + clearTripObjects(tripId) { + if (this.paths.has(tripId)) { + this.paths.get(tripId).setMap(null); + this.paths.delete(tripId); + } + + if (this.markers.has(tripId)) { + this.markers.get(tripId).forEach((marker) => marker.setMap(null)); + this.markers.delete(tripId); + } + + if (this.arrows.has(tripId)) { + this.arrows.get(tripId).forEach((arrow) => arrow.setMap(null)); + this.arrows.delete(tripId); + } + } + + clearAll() { + for (const tripId of this.paths.keys()) { + this.clearTripObjects(tripId); + } + } +} From 666c359f1e4efc763a3331f9e4994d83effd07e2 Mon Sep 17 00:00:00 2001 From: regeter <2320305+regeter@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:20:19 -0800 Subject: [PATCH 3/4] feat: Also add createTrip and updateTrip to trip related logs. --- src/TripLogs.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/TripLogs.js b/src/TripLogs.js index 16e5d4a..73acda8 100644 --- a/src/TripLogs.js +++ b/src/TripLogs.js @@ -446,10 +446,24 @@ class TripLogs { let tripIdx = 0; let nonTripIdx = 0; let lastTripStatus = "no status"; - // assumes logs are already sorted - // also assumes out-of-order updates can't happen. Unclear - // if this is a good assumption, but it might be worth it to call out - // places where it happens (since that might actually be a client bug). + + // First, create a map of trip IDs to their logs + const tripLogs = new Map(); + + // Collect all trip-related logs + _.forEach(this.rawLogs, (le) => { + if (le["@type"] === "createTrip" || le["@type"] === "updateTrip") { + const tripId = le.request?.tripid || _.get(le, "request.trip.name"); + if (tripId) { + if (!tripLogs.has(tripId)) { + tripLogs.set(tripId, []); + } + tripLogs.get(tripId).push(le); + } + } + }); + + // Process vehicle updates and create trip segments _.forEach(this.rawLogs, (le) => { if (le["@type"] === "updateVehicle" || le["@type"] === "updateDeliveryVehicle") { const newTripId = this.getSegmentID(le); @@ -457,6 +471,13 @@ class TripLogs { curTripId = newTripId; const tripName = newTripId ? newTripId : "non-trip-segment-" + nonTripIdx; curTripData = new Trip(tripIdx, tripName, new Date(le.timestamp)); + + // If this is an actual trip (not a non-trip-segment), add its logs + if (tripLogs.has(newTripId)) { + curTripData.logs = tripLogs.get(newTripId); + log(`Added ${curTripData.logs.length} logs to trip ${newTripId}`); + } + this.trips.push(curTripData); this.trip_ids.push(curTripData.tripName); @@ -479,7 +500,6 @@ class TripLogs { } } const tripStatus = _.get(le, "response.tripstatus"); - // if the logs had a trip status, and it changed update if (tripStatus && tripStatus !== lastTripStatus) { this.tripStatusChanges.push({ newStatus: tripStatus, From 4bff45288517fde84bcbb6e4baed2fdad117c1ad Mon Sep 17 00:00:00 2001 From: regeter <2320305+regeter@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:21:09 -0800 Subject: [PATCH 4/4] feat: Update Maps to use new TripObjects and draw pickup and dropoff markers --- src/Map.js | 93 ++++++------------------------------------ src/TrafficPolyline.js | 5 --- src/Trip.js | 33 +++++++++++++++ 3 files changed, 46 insertions(+), 85 deletions(-) diff --git a/src/Map.js b/src/Map.js index d153c23..0c1b79d 100644 --- a/src/Map.js +++ b/src/Map.js @@ -8,11 +8,11 @@ import Utils, { log } from "./Utils"; import PolylineCreation from "./PolylineCreation"; import { decode } from "s2polyline-ts"; import TrafficPolyline from "./TrafficPolyline"; +import { TripObjects } from "./TripObjects"; +import { getColor } from "./Trip"; let minDate; let maxDate; -let allPaths = []; -let allMarkers = []; let map; let apikey; let mapId; @@ -24,7 +24,6 @@ let panorama; let jwt; let projectId; let locationProvider; -let solutionType; let tripLogs; let taskLogs; let setFeaturedObject; @@ -37,62 +36,24 @@ const render = (status) => { }; function addTripPolys(map) { - _.forEach(allPaths, (p) => p.setMap(null)); - allPaths = []; - _.forEach(allMarkers, (m) => m.setMap(null)); - allMarkers = []; - + const tripObjects = new TripObjects({ + map, + setFeaturedObject, + setTimeRange, + }); const trips = tripLogs.getTrips(); const vehicleBounds = new window.google.maps.LatLngBounds(); - let lastVehicleCoords; + _.forEach(trips, (trip) => { + tripObjects.addTripVisuals(trip, minDate, maxDate); + + // Update bounds const tripCoords = trip.getPathCoords(minDate, maxDate); if (tripCoords.length > 0) { - lastVehicleCoords = _.last(tripCoords); - const path = new window.google.maps.Polyline({ - path: tripCoords, - geodesic: true, - strokeColor: getColor(trip.tripIdx), - strokeOpacity: 0.5, - strokeWeight: 6, - }); - google.maps.event.addListener(path, "mouseover", () => { - path.setOptions({ - strokeOpacity: 1, - strokeWeight: 8, - }); - }); - google.maps.event.addListener(path, "mouseout", () => { - path.setOptions({ - strokeOpacity: 0.5, - strokeWeight: 6, - }); - }); - google.maps.event.addListener(path, "click", () => { - const fd = trip.getFeaturedData(); - setFeaturedObject(fd); - // TODO: https://github.com/googlemaps/fleet-debugger/issues/79 - // this time range won't capture the createTrip logs - setTimeRange(fd.firstUpdate.getTime(), fd.lastUpdate.getTime()); - }); - getPolyBounds(vehicleBounds, path); - path.setMap(map); - allPaths.push(path); + tripCoords.forEach((coord) => vehicleBounds.extend(coord)); } }); - if (lastVehicleCoords) { - const urlBase = "http://maps.google.com/mapfiles/kml/shapes/"; - const lastVehicleLocMark = new window.google.maps.Marker({ - position: lastVehicleCoords, - map: map, - icon: { - url: urlBase + (solutionType === "LMFS" ? "truck.png" : "cabs.png"), - scaledSize: new google.maps.Size(18, 18), - }, - title: "Last Location", - }); - allMarkers.push(lastVehicleLocMark); - } + return vehicleBounds; } @@ -204,11 +165,6 @@ function MyMapComponent(props) { _.get(props.selectedRow, "lastlocation.currentroutesegment"); if (routeSegment) { - log("Processing new route segment polyline:", { - rowTimestamp: props.selectedRow.timestamp, - polyline: routeSegment, - }); - try { const decodedPoints = decode(routeSegment); @@ -218,12 +174,6 @@ function MyMapComponent(props) { lng: point.lngDegrees(), })); - log("Creating new polyline with", { - points: validWaypoints.length, - firstPoint: validWaypoints[0], - lastPoint: validWaypoints[validWaypoints.length - 1], - }); - const trafficRendering = _.get(props.selectedRow, "request.vehicle.currentroutesegmenttraffic.trafficrendering") || _.get(props.selectedRow, "lastlocation.currentroutesegmenttraffic.trafficrendering"); @@ -389,22 +339,6 @@ function MyMapComponent(props) { ); } -function getPolyBounds(bounds, p) { - p.getPath().forEach((e) => { - bounds.extend(e); - }); - return bounds; -} - -/* - * Deterministically assign a color per trip using tripIdx - * Colors were chosen for visibility - */ -function getColor(tripIdx) { - const colors = ["#2d7dd2", "#97cc04", "#eeb902", "#f45d01", "#474647", "00aa00"]; - return colors[tripIdx % colors.length]; -} - function Map(props) { tripLogs = props.logData.tripLogs; taskLogs = props.logData.taskLogs; @@ -415,7 +349,6 @@ function Map(props) { mapId = urlParams.get("mapId") || props.logData.mapId; jwt = props.logData.jwt; projectId = props.logData.projectId; - solutionType = props.logData.solutionType; setFeaturedObject = props.setFeaturedObject; setTimeRange = props.setTimeRange; diff --git a/src/TrafficPolyline.js b/src/TrafficPolyline.js index d06037b..75d57ae 100644 --- a/src/TrafficPolyline.js +++ b/src/TrafficPolyline.js @@ -57,11 +57,6 @@ export class TrafficPolyline { }, ...(trafficRendering.roadstretch || []), ]; - - log("Added traveled route segment:", { - lengthMeters: distanceInMeters, - segments: trafficRendering.roadstretch.length, - }); } } catch (error) { log("Error calculating traveled route segment:", error); diff --git a/src/Trip.js b/src/Trip.js index ba73ba4..8fe3d88 100644 --- a/src/Trip.js +++ b/src/Trip.js @@ -71,5 +71,38 @@ class Trip { getPlannedPath() { return this.plannedPath; } + + getPoint(type, path) { + return _.get( + _.find(this.logs, (log) => log["@type"] === type && _.get(log, path)), + path + ); + } + + getPickupPoint() { + return this.getPoint("createTrip", "request.trip.pickuppoint.point"); + } + + getDropoffPoint() { + return this.getPoint("createTrip", "request.trip.dropoffpoint.point"); + } + + getActualPickupPoint() { + return this.getPoint("updateTrip", "response.actualpickuppoint.point"); + } + + getActualDropoffPoint() { + return this.getPoint("updateTrip", "response.actualdropoffpoint.point"); + } } + +/* + * Deterministically assign a color per trip using tripIdx + * Colors were chosen for visibility + */ +export function getColor(tripIdx) { + const colors = ["#2d7dd2", "#97cc04", "#eeb902", "#f45d01", "#474647", "00aa00"]; + return colors[tripIdx % colors.length]; +} + export { Trip as default };