diff --git a/src/App.js b/src/App.js
index 7d08d6c..4a6f877 100644
--- a/src/App.js
+++ b/src/App.js
@@ -22,17 +22,7 @@ import "./global.css";
import { log } from "./Utils";
import { toast, ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
-
-/**
- * returns the default value for the button from the url
- */
-function getToggleDefault(urlKey, defaultVal) {
- const urlVal = getQueryStringValue(urlKey);
- if (urlVal === "true") {
- defaultVal = true;
- }
- return defaultVal;
-}
+import { ALL_TOGGLES, getVisibleToggles } from "./MapToggles";
class App extends React.Component {
constructor(props) {
@@ -41,7 +31,7 @@ class App extends React.Component {
const nowDate = new Date();
let urlMinTime = getQueryStringValue("minTime");
let urlMaxTime = getQueryStringValue("maxTime");
- this.initialMinTime = urlMinTime ? parseInt(urlMinTime) : 0; // default max time to 1 year in the future
+ this.initialMinTime = urlMinTime ? parseInt(urlMinTime) : 0;
this.initialMaxTime = urlMaxTime ? parseInt(urlMaxTime) : nowDate.setFullYear(nowDate.getFullYear() + 1);
this.focusOnRowFunction = null;
this.state = {
@@ -50,22 +40,7 @@ class App extends React.Component {
playSpeed: 1000,
featuredObject: { msg: "Click a table row to select object" },
extraColumns: [],
- toggleOptions: {
- showGPSBubbles: getToggleDefault("showGPSBubbles", false),
- showHeading: getToggleDefault("showHeading", false),
- showSpeed: getToggleDefault("showSpeed", false),
- showTraffic: getToggleDefault("showTraffic", false),
- showTripStatus: getToggleDefault("showTripStatus", false),
- showDwellLocations: getToggleDefault("showDwellLocations", false),
- showNavStatus: getToggleDefault("showNavStatus", false),
- showETADeltas: getToggleDefault("showETADeltas", false),
- showHighVelocityJumps: getToggleDefault("showHighVelocityJumps", false),
- showMissingUpdates: getToggleDefault("showMissingUpdates", false),
- showTasksAsCreated: getToggleDefault("showTasksAsCreated", false),
- showPlannedPaths: getToggleDefault("showPlannedPaths", false),
- showLiveJS: getToggleDefault("showLiveJS", false),
- showClientServerTimeDeltas: getToggleDefault("showClientServerTimeDeltas", false),
- },
+ toggleOptions: Object.fromEntries(ALL_TOGGLES.map((t) => [t.id, false])),
uploadedDatasets: [null, null, null, null, null],
activeDatasetIndex: null,
activeMenuIndex: null,
@@ -73,139 +48,17 @@ class App extends React.Component {
selectedRowIndexPerDataset: [-1, -1, -1, -1, -1],
currentLogData: this.props.logData,
};
- // Realtime updates are too heavy. There must be a better/ react way
this.onSliderChangeDebounced = _.debounce((timeRange) => this.onSliderChange(timeRange), 25);
-
- // TODO: refactor so that visualizations are registered
- // rather than enumerated here?
- this.toggles = _.filter(
- [
- {
- id: "showGPSBubbles",
- name: "GPS Accuracy",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/GPSAccuracy.md",
- columns: ["lastlocation.rawlocationaccuracy", "lastlocation.locationsensor"],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showHeading",
- name: "Heading",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Heading.md",
- columns: ["lastlocation.heading", "lastlocation.headingaccuracy"],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showSpeed",
- name: "Speed",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Speed.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showTripStatus",
- name: "Trip Status",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/TripStatus.md",
- columns: [],
- solutionTypes: ["ODRD"],
- },
- {
- id: "showNavStatus",
- name: "Navigation Status",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/NavStatus.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showTasksAsCreated",
- name: "Tasks",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Tasks.md",
- columns: [],
- solutionTypes: ["LMFS"],
- },
- {
- id: "showPlannedPaths",
- name: "Planned Paths",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/PlannedPaths.md",
- columns: [],
- solutionTypes: ["LMFS"],
- },
- {
- id: "showDwellLocations",
- name: "Dwell Locations",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/DwellTimes.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showHighVelocityJumps",
- name: "Jumps (unrealistic velocity)",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/VelocityJumps.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showMissingUpdates",
- name: "Jumps (Temporal)",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/MissingUpdates.md",
- columns: ["temporal_gap"],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showClientServerTimeDeltas",
- name: "Client/Server Time Deltas",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showETADeltas",
- name: "ETA Deltas",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/EtaDeltas.md",
- columns: ["request.vehicle.etatofirstwaypoint"],
- solutionTypes: ["ODRD"],
- },
- {
- id: "showTraffic",
- name: "Live Traffic",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- {
- id: "showLiveJS",
- name: "Live Journey Sharing",
- docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
- columns: [],
- solutionTypes: ["ODRD", "LMFS"],
- },
- ],
- (toggle) => {
- return toggle.solutionTypes.indexOf(this.state.currentLogData.solutionType) !== -1;
- }
- );
this.setFeaturedObject = this.setFeaturedObject.bind(this);
this.setTimeRange = this.setTimeRange.bind(this);
}
- /*
- * Update react state from data in the url. This could/should be
- * cleaned up. The pure react state is actually set properly in the
- * constructor ... all this does is update the map and associated
- * data (once it's loaded). Given this split it's definitely possible
- * that this just overwrites settings a quickfingered user already
- * changed.
- */
updateMapAndAssociatedData = () => {
this.setTimeRange(this.state.timeRange.minTime, this.state.timeRange.maxTime);
- _.map(this.toggles, (toggle) => {
- const urlVal = getQueryStringValue(toggle.id);
- if (urlVal === "true") {
- this.updateToggleState(true, toggle.id, toggle.columns);
- }
- });
};
componentDidMount() {
+ log(`Initial device pixel ratio: ${window.devicePixelRatio}`);
this.initializeData().then(() => {
this.updateMapAndAssociatedData();
});
@@ -221,34 +74,27 @@ class App extends React.Component {
updateToggleState(newValue, toggleName, jsonPaths) {
this.setState((prevState) => {
- prevState.toggleOptions[toggleName] = newValue;
- setQueryStringValue(toggleName, newValue);
- const extraColumns = _.clone(prevState.extraColumns);
- _.forEach(jsonPaths, (path) => {
- if (newValue) {
- extraColumns.push(path);
- } else {
- _.pull(extraColumns, path);
- }
- });
- prevState.extraColumns = _.uniq(extraColumns);
- return prevState;
+ const newToggleOptions = {
+ ...prevState.toggleOptions,
+ [toggleName]: newValue,
+ };
+
+ const newExtraColumns = newValue
+ ? _.union(prevState.extraColumns, jsonPaths)
+ : _.difference(prevState.extraColumns, jsonPaths);
+
+ return {
+ toggleOptions: newToggleOptions,
+ extraColumns: newExtraColumns,
+ };
});
}
- /*
- * Updates react state associated with the slider and calls into
- * the non-react map code to do the same.
- */
onSliderChange(timeRange) {
this.setTimeRange(timeRange.minTime, timeRange.maxTime);
}
- /*
- * Callback to updated selected log row
- */
onSelectionChange(selectedRow, rowIndex) {
- // Save both the selected row and its index for the current dataset
if (this.state.activeDatasetIndex !== null && rowIndex !== undefined) {
this.setState((prevState) => {
const newSelectedIndexes = [...prevState.selectedRowIndexPerDataset];
@@ -264,9 +110,6 @@ class App extends React.Component {
}
}
- /*
- * Set the featured object
- */
setFeaturedObject(featuredObject) {
this.setState({ featuredObject: featuredObject });
}
@@ -281,9 +124,6 @@ class App extends React.Component {
}
};
- /*
- * exposes editing of the timeRange state
- */
setTimeRange(minTime, maxTime, callback) {
setQueryStringValue("minTime", minTime);
setQueryStringValue("maxTime", maxTime);
@@ -818,29 +658,22 @@ class App extends React.Component {
log(`Switched to dataset ${index}`);
log(`New time range: ${tripLogs.minDate} - ${tripLogs.maxDate}`);
- // After dataset is loaded, try to restore the previously selected row index
const savedRowIndex = this.state.selectedRowIndexPerDataset[index];
log(`Attempting to restore row at index ${savedRowIndex} for dataset ${index}`);
- // Wait for map and components to fully initialize
setTimeout(() => {
if (savedRowIndex >= 0) {
- // Get current log data with the new time range
const minDate = new Date(this.state.timeRange.minTime);
const maxDate = new Date(this.state.timeRange.maxTime);
const logs = tripLogs.getLogs_(minDate, maxDate).value();
- // Check if the saved index is valid for the current dataset
if (savedRowIndex < logs.length) {
log(`Restoring row at index ${savedRowIndex}`);
const rowToSelect = logs[savedRowIndex];
- // First update the featured object
this.setState({ featuredObject: rowToSelect }, () => {
- // Then focus on the row in the table
this.focusOnSelectedRow();
- // And finally center the map on the location (simulating a long press)
const lat = _.get(rowToSelect, "lastlocation.rawlocation.latitude");
const lng = _.get(rowToSelect, "lastlocation.rawlocation.longitude");
@@ -856,13 +689,11 @@ class App extends React.Component {
this.selectFirstRow();
}
} else {
- // If no saved selection or invalid index, select first row
log(`No previously saved row index for dataset ${index}, selecting first row`);
this.selectFirstRow();
}
- }, 300); // Increased delay to ensure map is fully initialized
+ }, 300);
- // Update map and associated data
this.updateMapAndAssociatedData();
}
);
@@ -875,15 +706,15 @@ class App extends React.Component {
};
toggleClickHandler(id) {
- const toggle = _.find(this.toggles, { id });
+ const toggle = _.find(ALL_TOGGLES, { id });
const newValue = !this.state.toggleOptions[id];
this.updateToggleState(newValue, id, toggle.columns);
}
render() {
- const selectedEventTime = this.state.featuredObject?.timestamp
- ? new Date(this.state.featuredObject.timestamp).getTime()
- : null;
+ const { featuredObject, timeRange, currentLogData, toggleOptions, extraColumns } = this.state;
+ const selectedEventTime = featuredObject?.timestamp ? new Date(featuredObject.timestamp).getTime() : null;
+ const visibleToggles = getVisibleToggles(currentLogData.solutionType);
return (
@@ -893,12 +724,12 @@ class App extends React.Component {
this.onSelectionChange(row, rowIndex)}
@@ -917,8 +748,8 @@ class App extends React.Component {
focusSelectedRow={this.focusOnSelectedRow}
/>
this.toggleClickHandler(id)}
/>
@@ -959,10 +790,10 @@ class App extends React.Component {
this.onSelectionChange(rowData, rowIndex)}
setFocusOnRowFunction={this.setFocusOnRowFunction}
centerOnLocation={this.centerOnLocation}
@@ -970,10 +801,7 @@ class App extends React.Component {
- this.onDataframePropClick(select)}
- />
+ this.onDataframePropClick(select)} />
);
diff --git a/src/Map.js b/src/Map.js
index 3209c53..1955863 100644
--- a/src/Map.js
+++ b/src/Map.js
@@ -1,483 +1,369 @@
// src/Map.js
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useRef, useState, useMemo, useCallback } from "react";
import { APIProvider, useMapsLibrary } from "@vis.gl/react-google-maps";
import _ from "lodash";
import { getQueryStringValue, setQueryStringValue } from "./queryString";
-import Utils, { log } from "./Utils";
+import { log } from "./Utils";
import PolylineCreation from "./PolylineCreation";
import { decode } from "s2polyline-ts";
import TrafficPolyline from "./TrafficPolyline";
import { TripObjects } from "./TripObjects";
-import { getColor } from "./Trip";
+import { getToggleHandlers } from "./MapToggles.js";
+
+function MapComponent({
+ logData,
+ rangeStart,
+ rangeEnd,
+ selectedRow,
+ toggles,
+ toggleOptions,
+ setFeaturedObject,
+ setTimeRange,
+ focusSelectedRow,
+ initialMapBounds,
+ setCenterOnLocation,
+}) {
+ const { tripLogs, taskLogs, jwt, projectId, mapId } = logData;
+
+ const mapDivRef = useRef(null);
+ const mapRef = useRef(null);
+ const locationProviderRef = useRef(null);
+ const bubbleMapRef = useRef({});
+ const trafficLayerRef = useRef(null);
+ const dataMakersRef = useRef([]);
+ const trafficPolylinesRef = useRef([]);
+ const panoramaRef = useRef(null);
-let minDate;
-let maxDate;
-let map;
-let apikey;
-let mapId;
-let dataMakers = [];
-let trafficLayer;
-const bubbleMap = {};
-const toggleHandlers = {};
-let panorama;
-let jwt;
-let projectId;
-let locationProvider;
-let tripLogs;
-let taskLogs;
-let setFeaturedObject;
-let focusSelectedRow;
-let setTimeRange;
-
-function addTripPolys(map) {
- const tripObjects = new TripObjects({
- map,
- setFeaturedObject,
- setTimeRange,
- });
- const trips = tripLogs.getTrips();
- const vehicleBounds = new window.google.maps.LatLngBounds();
-
- // Add click handler to map for finding nearby events
- map.addListener("click", (event) => {
- const clickLocation = event.latLng;
- log("Map click detected at location:", clickLocation.lat(), clickLocation.lng());
-
- // Find the closest event within 250 meters
- const closestEvent = findClosestEvent(clickLocation, 250);
- if (closestEvent) {
- log("Found closest event:", closestEvent.timestamp);
- setFeaturedObject(closestEvent);
-
- setTimeout(() => focusSelectedRow(), 0);
- }
- });
+ const [showPolylineUI, setShowPolylineUI] = useState(false);
+ const [_polylines, setPolylines] = useState([]);
+ const [buttonPosition, setButtonPosition] = useState({ top: 0, left: 0 });
+ const [isFollowingVehicle, setIsFollowingVehicle] = useState(false);
+ const lastValidPositionRef = useRef(null);
- _.forEach(trips, (trip) => {
- tripObjects.addTripVisuals(trip, minDate, maxDate);
- // Update bounds
- const tripCoords = trip.getPathCoords(minDate, maxDate);
- if (tripCoords.length > 0) {
- tripObjects.addTripVisuals(trip, minDate, maxDate);
- tripCoords.forEach((coord) => vehicleBounds.extend(coord));
- }
- });
+ const minDate = useMemo(() => new Date(rangeStart), [rangeStart]);
+ const maxDate = useMemo(() => new Date(rangeEnd), [rangeEnd]);
- return vehicleBounds;
-}
+ // Effect for map initialization (runs once on mount)
+ useEffect(() => {
+ if (!mapDivRef.current) return;
+ log("Map.js: Initialization useEffect triggered.");
-/*
- * Creates the map object using a journeySharing location
- * provider.
- */
-function initializeMapObject(element) {
- // In a more normal implementation authTokenFetcher
- // would actually be making a RPC to a backend to generate
- // the jwt. For debugging use cases the jwt gets bundled into
- // the extracted log data.
- function authTokenFetcher(options) {
- // TODO #25 - bake in actual expiration time -- and give a
- // better error message for expired jwts
- console.log("Ignoring options using prebuilt jwt", options);
- const authToken = {
- token: jwt,
+ const authTokenFetcher = (options) => {
+ log("Ignoring options; using pre-built JWT.", options);
+ return { token: jwt };
};
- return authToken;
- }
-
- locationProvider = new window.google.maps.journeySharing.FleetEngineTripLocationProvider({
- projectId,
- authTokenFetcher,
- });
- const jsMapView = new window.google.maps.journeySharing.JourneySharingMapView({
- element: element,
- locationProviders: [locationProvider],
- mapOptions: {
- mapId: mapId,
- mapTypeControl: true,
- streetViewControl: true,
- },
- });
- const map = jsMapView.map;
- return map;
-}
+ locationProviderRef.current = new window.google.maps.journeySharing.FleetEngineTripLocationProvider({
+ projectId,
+ authTokenFetcher,
+ });
-function MyMapComponent(props) {
- const ref = useRef();
- const [showPolylineUI, setShowPolylineUI] = useState(false);
- const [polylines, setPolylines] = useState([]);
- const [buttonPosition, setButtonPosition] = useState({ top: 0, left: 0 });
- const [isFollowingVehicle, setIsFollowingVehicle] = useState(false);
- const lastValidPositionRef = useRef(null);
+ const jsMapView = new window.google.maps.journeySharing.JourneySharingMapView({
+ element: mapDivRef.current,
+ locationProviders: [locationProviderRef.current],
+ mapOptions: { mapId, mapTypeControl: true, streetViewControl: true },
+ });
+ const map = jsMapView.map;
+ mapRef.current = map;
+
+ const tripObjects = new TripObjects({ map, setFeaturedObject, setTimeRange });
+
+ const addTripPolys = () => {
+ const trips = tripLogs.getTrips();
+ const vehicleBounds = new window.google.maps.LatLngBounds();
+ _.forEach(trips, (trip) => {
+ tripObjects.addTripVisuals(trip, minDate, maxDate);
+ const tripCoords = trip.getPathCoords(minDate, maxDate);
+ if (tripCoords.length > 0) {
+ tripCoords.forEach((coord) => vehicleBounds.extend(coord));
+ }
+ });
+ return vehicleBounds;
+ };
- useEffect(() => {
+ // Set initial view
const urlZoom = getQueryStringValue("zoom");
const urlCenter = getQueryStringValue("center");
- const urlHeading = getQueryStringValue("heading");
- map = initializeMapObject(ref.current);
- addTripPolys(map);
-
if (urlZoom && urlCenter) {
- log("setting zoom & center from url", urlZoom, urlCenter);
map.setZoom(parseInt(urlZoom));
map.setCenter(JSON.parse(urlCenter));
- } else if (props.initialMapBounds) {
- log("Fitting map to pre-calculated dataset bounds.");
- const { north, south, east, west } = props.initialMapBounds;
+ addTripPolys();
+ } else if (initialMapBounds) {
+ const { north, south, east, west } = initialMapBounds;
const bounds = new window.google.maps.LatLngBounds(
new window.google.maps.LatLng(south, west),
new window.google.maps.LatLng(north, east)
);
map.fitBounds(bounds);
+ addTripPolys();
} else {
- log("No bounds provided, defaulting to a world view.");
- map.setCenter({ lat: 20, lng: 0 });
- map.setZoom(2);
- }
-
- if (urlHeading) {
- map.setHeading(parseInt(urlHeading));
+ const vehicleBounds = addTripPolys();
+ if (!vehicleBounds.isEmpty()) {
+ map.fitBounds(vehicleBounds);
+ } else {
+ map.setCenter({ lat: 20, lng: 0 });
+ map.setZoom(2);
+ }
}
- map.setOptions({ maxZoom: 100 });
-
- map.addListener("heading_changed", () => {
- setQueryStringValue("heading", map.getHeading());
- });
-
- map.addListener("dragstart", () => {
- setIsFollowingVehicle(false);
- log("Follow mode disabled due to map drag");
- });
-
- map.addListener(
- "center_changed",
- _.debounce(() => {
- const center = map.getCenter();
- if (center) {
- console.log("center_changed event fired, updating query string.");
- setQueryStringValue("center", JSON.stringify(center.toJSON()));
- }
- }, 100)
- );
+ // Add UI Controls
const polylineButton = document.createElement("button");
polylineButton.textContent = "Add Polyline";
polylineButton.className = "map-button";
-
polylineButton.onclick = (event) => {
+ log("Add Polyline button clicked.");
const rect = event.target.getBoundingClientRect();
setButtonPosition({ top: rect.bottom, left: rect.left });
setShowPolylineUI((prev) => !prev);
- log("Polyline button clicked");
};
-
map.controls[window.google.maps.ControlPosition.TOP_LEFT].push(polylineButton);
- // Create follow vehicle button with chevron icon
const followButton = document.createElement("div");
followButton.className = "follow-vehicle-button";
- followButton.innerHTML = `
-
-
- `;
-
+ followButton.innerHTML = ``;
followButton.onclick = () => {
- log("Follow vehicle button clicked");
- recenterOnVehicle();
+ log("Follow vehicle button clicked.");
+ recenterOnVehicleWrapper();
+ map.setZoom(17);
};
-
- // Add button to map
map.controls[window.google.maps.ControlPosition.LEFT_BOTTOM].push(followButton);
- updateFollowButtonAppearance();
- }, []);
-
- useEffect(() => {
- updateFollowButtonAppearance();
- }, [isFollowingVehicle]);
-
- const updateFollowButtonAppearance = () => {
- const followButton = document.querySelector(".follow-vehicle-button");
- if (followButton) {
- if (isFollowingVehicle) {
- followButton.classList.add("active");
- log("Follow vehicle button updated to active state");
- } else {
- followButton.classList.remove("active");
- log("Follow vehicle button updated to inactive state");
+ const clickListener = map.addListener("click", (event) => {
+ log("Map click listener triggered.");
+ const clickLocation = event.latLng;
+ const logs = tripLogs.getLogs_(new Date(rangeStart), new Date(rangeEnd)).value();
+ let closestEvent = null;
+ let closestDistance = 250;
+
+ logs.forEach((logEntry) => {
+ const rawLocation = _.get(logEntry, "lastlocation.rawlocation");
+ if (rawLocation?.latitude && rawLocation?.longitude) {
+ const eventLocation = new window.google.maps.LatLng(rawLocation.latitude, rawLocation.longitude);
+ const distance = window.google.maps.geometry.spherical.computeDistanceBetween(clickLocation, eventLocation);
+ if (distance < closestDistance) {
+ closestEvent = logEntry;
+ closestDistance = distance;
+ }
+ }
+ });
+ if (closestEvent) {
+ setFeaturedObject(closestEvent);
+ setTimeout(() => focusSelectedRow(), 0);
}
- }
- };
+ });
- const handlePolylineSubmit = (waypoints, properties) => {
- const path = waypoints.map((wp) => new window.google.maps.LatLng(wp.latitude, wp.longitude));
+ const centerListener = map.addListener(
+ "center_changed",
+ _.debounce(() => {
+ if (mapRef.current) setQueryStringValue("center", JSON.stringify(mapRef.current.getCenter().toJSON()));
+ }, 100)
+ );
+ const headingListener = map.addListener("heading_changed", () => {
+ if (mapRef.current) setQueryStringValue("heading", mapRef.current.getHeading());
+ });
- const arrowIcon = {
- path: window.google.maps.SymbolPath.FORWARD_OPEN_ARROW,
- scale: properties.strokeWeight / 2,
- strokeColor: properties.color,
+ return () => {
+ log("Map.js: Cleanup from initialization useEffect.");
+ window.google.maps.event.removeListener(clickListener);
+ window.google.maps.event.removeListener(centerListener);
+ window.google.maps.event.removeListener(headingListener);
+ mapRef.current = null;
};
+ }, []);
- const polyline = new window.google.maps.Polyline({
- path: path,
- geodesic: true,
- strokeColor: properties.color,
- strokeOpacity: properties.opacity,
- strokeWeight: properties.strokeWeight,
- icons: [
- {
- icon: arrowIcon,
- offset: "0%",
- },
- {
- icon: arrowIcon,
- offset: "100%",
- },
- ],
- });
+ const handlePolylineSubmit = useCallback((waypoints, properties) => {
+ const map = mapRef.current;
+ if (!map) return;
+ log("handlePolylineSubmit called.");
- polyline.setMap(map);
- setPolylines([...polylines, polyline]);
- log(
- `Polyline ${polylines.length + 1} created with color: ${properties.color}, opacity: ${
- properties.opacity
- }, stroke weight: ${properties.strokeWeight}`
- );
- };
+ const path = waypoints.map((wp) => new window.google.maps.LatLng(wp.latitude, wp.longitude));
+ const newPolyline = new window.google.maps.Polyline({ path, geodesic: true, ...properties });
+ newPolyline.setMap(map);
+ setPolylines((prev) => [...prev, newPolyline]);
+ }, []);
- const recenterOnVehicle = () => {
- log("Executing recenterOnVehicle function");
+ const recenterOnVehicleWrapper = useCallback(() => {
+ const map = mapRef.current;
+ if (!map) return;
+ log("recenterOnVehicleWrapper called for follow mode.");
let position = null;
-
- // Try to get position from current row
- if (props.selectedRow && props.selectedRow.lastlocation && props.selectedRow.lastlocation.rawlocation) {
- position = props.selectedRow.lastlocation.rawlocation;
- log(`Found position in selected row: ${position.latitude}, ${position.longitude}`);
- }
- // Try to use our last cached valid position
- else if (lastValidPositionRef.current) {
+ if (selectedRow?.lastlocation?.rawlocation) {
+ position = selectedRow.lastlocation.rawlocation;
+ } else if (lastValidPositionRef.current) {
position = lastValidPositionRef.current;
- log(`Using last cached valid position: ${position.lat}, ${position.lng}`);
- }
- if (!position) {
- log("No vehicle position found");
- }
-
- // Center the map
- if (position && typeof position.latitude !== "undefined") {
- map.setCenter({ lat: position.latitude, lng: position.longitude });
- map.setZoom(17);
}
- setIsFollowingVehicle((prev) => {
- const newState = !prev;
- log(`Map follow mode ${newState ? "enabled" : "disabled"}`);
- return newState;
- });
-
- log(`Map centered on vehicle, follow mode ${!isFollowingVehicle ? "enabled" : "disabled"}`);
- };
+ if (position) map.setCenter({ lat: position.latitude, lng: position.longitude });
+ setIsFollowingVehicle((prev) => !prev);
+ }, [selectedRow]);
- /*
- * Handler for timewindow change. Updates global min/max date globals
- * and recomputes the paths as well as all the bubble markers to respect the
- * new date values.
- *
- * Debounced to every 100ms as a blance between performance and reactivity when
- * the slider is dragged.
- */
useEffect(() => {
- const updateMap = () => {
- minDate = new Date(props.rangeStart);
- maxDate = new Date(props.rangeEnd);
- addTripPolys(map);
- _.forEach(toggleHandlers, (handler, name) => {
- if (bubbleMap[name]) {
- handler(true);
- }
- });
- };
-
- // Create a debounced version of updateMap
- const debouncedUpdateMap = _.debounce(updateMap, 200);
- debouncedUpdateMap();
-
- // Cleanup function to cancel any pending debounced calls when the effect re-runs or unmounts
- return () => {
- debouncedUpdateMap.cancel();
- };
- }, [props.rangeStart, props.rangeEnd]);
+ const followButton = document.querySelector(".follow-vehicle-button");
+ if (followButton) {
+ isFollowingVehicle ? followButton.classList.add("active") : followButton.classList.remove("active");
+ }
+ }, [isFollowingVehicle]);
+ // Effect to draw traffic polyline for selected row
useEffect(() => {
- if (!props.selectedRow) return;
-
- // Clear ALL route segment polylines
- polylines.forEach((polyline) => {
- if (polyline.isRouteSegment) {
- polyline.setMap(null);
- }
- });
- // Update the polylines state to remove all route segments
- setPolylines(polylines.filter((p) => !p.isRouteSegment));
+ const map = mapRef.current;
+ if (!map) return;
- const eventType = props.selectedRow["@type"];
- const isTripEvent = ["getTrip", "updateTrip", "createTrip"].includes(eventType);
+ trafficPolylinesRef.current.forEach((p) => p.setMap(null));
+ trafficPolylinesRef.current = [];
- if (isTripEvent) {
- // Create a thin red polyline with arrows for trip events
- const routeSegment = _.get(props.selectedRow, "response.currentroutesegment");
- if (routeSegment) {
- try {
- const decodedPoints = decode(routeSegment);
- if (decodedPoints && decodedPoints.length > 0) {
- const validWaypoints = decodedPoints.map((point) => ({
- lat: point.latDegrees(),
- lng: point.lngDegrees(),
- }));
-
- const trafficPolyline = new TrafficPolyline({
- path: validWaypoints,
- zIndex: 3,
- isTripEvent: true,
- map: map,
- });
- setPolylines((prev) => [...prev, ...trafficPolyline.polylines]);
- }
- } catch (error) {
- console.error("Error processing trip event polyline:", error);
- }
- }
- }
+ if (!selectedRow) return;
const routeSegment =
- _.get(props.selectedRow, "request.vehicle.currentroutesegment") ||
- _.get(props.selectedRow, "lastlocation.currentroutesegment");
-
+ _.get(selectedRow, "request.vehicle.currentroutesegment") ||
+ _.get(selectedRow, "lastlocation.currentroutesegment");
if (routeSegment) {
try {
const decodedPoints = decode(routeSegment);
-
- if (decodedPoints && decodedPoints.length > 0) {
- const validWaypoints = decodedPoints.map((point) => ({
- lat: point.latDegrees(),
- lng: point.lngDegrees(),
- }));
-
+ if (decodedPoints?.length > 0) {
+ const validWaypoints = decodedPoints.map((p) => ({ lat: p.latDegrees(), lng: p.lngDegrees() }));
const trafficRendering =
- _.get(props.selectedRow, "request.vehicle.currentroutesegmenttraffic.trafficrendering") ||
- _.get(props.selectedRow, "lastlocation.currentroutesegmenttraffic.trafficrendering");
-
- const location = _.get(props.selectedRow.lastlocation, "location");
+ _.get(selectedRow, "request.vehicle.currentroutesegmenttraffic.trafficrendering") ||
+ _.get(selectedRow, "lastlocation.currentroutesegmenttraffic.trafficrendering");
+ const location = _.get(selectedRow.lastlocation, "location");
const trafficPolyline = new TrafficPolyline({
path: validWaypoints,
+ map,
zIndex: 2,
trafficRendering: structuredClone(trafficRendering),
currentLatLng: location,
- map: map,
});
- setPolylines((prev) => [...prev, ...trafficPolyline.polylines]);
+
+ trafficPolylinesRef.current = trafficPolyline.polylines;
}
} catch (error) {
console.error("Error processing route segment polyline:", error);
}
}
- }, [props.selectedRow]);
+ }, [selectedRow]);
+ // Effect for updating selected row vehicle marker
useEffect(() => {
- // Vehicle chevron location maker
- const data = props.selectedRow;
- if (!data) return;
- _.forEach(dataMakers, (m) => m.setMap(null));
- dataMakers = [];
+ const map = mapRef.current;
+ dataMakersRef.current.forEach((m) => m.setMap(null));
+ dataMakersRef.current = [];
- const markerSymbols = {
- background: {
- path: window.google.maps.SymbolPath.CIRCLE,
- fillColor: "#FFFFFF",
- fillOpacity: 0.7,
- scale: 18,
- strokeColor: "#FFFFFF",
- strokeWeight: 2,
- strokeOpacity: 0.3,
- },
- chevron: {
- path: "M -1,1 L 0,-1 L 1,1 L 0,0.5 z",
- fillColor: "#4285F4",
- fillOpacity: 1,
- scale: 10,
- strokeColor: "#4285F4",
- strokeWeight: 1,
- rotation: 0,
- },
- rawLocation: {
- path: window.google.maps.SymbolPath.CIRCLE,
- fillColor: "#FF0000",
- fillOpacity: 1,
- scale: 2,
- strokeColor: "#FF0000",
- strokeWeight: 1,
- },
- };
+ if (!map || !selectedRow) return;
- const location = _.get(data.lastlocation, "location") || _.get(data.lastlocationResponse, "location");
- if (location) {
- lastValidPositionRef.current = { lat: location.latitude, lng: location.longitude };
+ const location = _.get(selectedRow.lastlocation, "location") || _.get(selectedRow.lastlocationResponse, "location");
- const heading = _.get(data.lastlocation, "heading") || _.get(data.lastlocationResponse, "heading") || 0;
- markerSymbols.chevron.rotation = heading;
+ if (location?.latitude && location?.longitude) {
+ const pos = { lat: location.latitude, lng: location.longitude };
+ lastValidPositionRef.current = pos;
+ const heading =
+ _.get(selectedRow.lastlocation, "heading") || _.get(selectedRow.lastlocationResponse, "heading") || 0;
const backgroundMarker = new window.google.maps.Marker({
- position: { lat: location.latitude, lng: location.longitude },
- map: map,
- icon: markerSymbols.background,
- clickable: false,
+ position: pos,
+ map,
+ icon: {
+ path: window.google.maps.SymbolPath.CIRCLE,
+ fillColor: "#FFFFFF",
+ fillOpacity: 0.7,
+ scale: 18,
+ strokeColor: "#FFFFFF",
+ strokeWeight: 2,
+ strokeOpacity: 0.3,
+ },
zIndex: 9,
});
-
const chevronMarker = new window.google.maps.Marker({
- position: { lat: location.latitude, lng: location.longitude },
- map: map,
- icon: markerSymbols.chevron,
+ position: pos,
+ map,
+ icon: {
+ path: "M -1,1 L 0,-1 L 1,1 L 0,0.5 z",
+ fillColor: "#4285F4",
+ fillOpacity: 1,
+ scale: 10,
+ strokeColor: "#4285F4",
+ strokeWeight: 1,
+ rotation: heading,
+ },
zIndex: 10,
});
+ dataMakersRef.current.push(backgroundMarker, chevronMarker);
- dataMakers.push(backgroundMarker, chevronMarker);
-
- const rawLocation = _.get(data.lastlocation, "rawlocation");
- if (rawLocation) {
+ const rawLocation = _.get(selectedRow.lastlocation, "rawlocation");
+ if (rawLocation?.latitude && rawLocation?.longitude) {
+ const rawPos = { lat: rawLocation.latitude, lng: rawLocation.longitude };
const rawLocationMarker = new window.google.maps.Marker({
- position: { lat: rawLocation.latitude, lng: rawLocation.longitude },
- map: map,
- icon: markerSymbols.rawLocation,
+ position: rawPos,
+ map,
+ icon: {
+ path: window.google.maps.SymbolPath.CIRCLE,
+ fillColor: "#FF0000",
+ fillOpacity: 1,
+ scale: 2,
+ strokeColor: "#FF0000",
+ strokeWeight: 1,
+ },
zIndex: 8,
- clickable: false,
});
-
- dataMakers.push(rawLocationMarker);
+ dataMakersRef.current.push(rawLocationMarker);
}
if (isFollowingVehicle) {
- map.setCenter({ lat: location.latitude, lng: location.longitude });
+ map.setCenter(pos);
+ }
+ }
+ }, [selectedRow, isFollowingVehicle]);
+
+ const toggleHandlers = useMemo(() => {
+ const map = mapRef.current;
+ if (!map) {
+ return {};
+ }
+ return getToggleHandlers({
+ map,
+ tripLogs,
+ taskLogs,
+ minDate,
+ maxDate,
+ setFeaturedObject,
+ setTimeRange,
+ bubbleMapRef,
+ panoramaRef,
+ mapDivRef,
+ trafficLayerRef,
+ locationProviderRef,
+ jwt,
+ });
+ }, [mapRef.current, tripLogs, taskLogs, minDate, maxDate, jwt, setFeaturedObject, setTimeRange]);
+
+ useEffect(() => {
+ if (_.isEmpty(toggleHandlers)) {
+ log("Map.js: Toggles effect skipped because handlers are not ready.");
+ return;
+ }
+ for (const toggle of toggles) {
+ if (toggleHandlers[toggle.id]) {
+ toggleHandlers[toggle.id](toggleOptions[toggle.id]);
}
}
- }, [props.selectedRow, isFollowingVehicle]);
+ }, [toggleOptions, toggles, toggleHandlers, minDate, maxDate]);
- for (const toggle of props.toggles) {
- const id = toggle.id;
- const enabled = props.toggleOptions[id];
- useEffect(() => {
- toggleHandlers[id](enabled);
- }, [props.toggleOptions[id]]);
- }
+ useEffect(() => {
+ const centerOnLocationImpl = (lat, lng) => {
+ const map = mapRef.current;
+ if (map) {
+ log(`Centering map on ${lat}, ${lng} with zoom 13.`);
+ map.setCenter({ lat, lng });
+ map.setZoom(13);
+ }
+ };
+ setCenterOnLocation(centerOnLocationImpl);
+ }, [setCenterOnLocation]);
return (
<>
-
+
{showPolylineUI && (
Loading Maps...;
- }
-
- log("MapContent: Google Maps libraries loaded, rendering MyMapComponent.");
- return ;
+ if (!journeySharingLib || !geometryLib) return Loading Maps...
;
+ return ;
}
-function Map(props) {
- tripLogs = props.logData.tripLogs;
- taskLogs = props.logData.taskLogs;
- minDate = props.rangeStart;
- maxDate = props.rangeEnd;
- const urlParams = new URLSearchParams(window.location.search);
- apikey = urlParams.get("apikey") || props.logData.apikey;
- mapId = urlParams.get("mapId") || props.logData.mapId;
- jwt = props.logData.jwt;
- projectId = props.logData.projectId;
- setFeaturedObject = props.setFeaturedObject;
- focusSelectedRow = props.focusSelectedRow;
- setTimeRange = props.setTimeRange;
-
- function centerOnLocation(lat, lng) {
- if (map) {
- const newCenter = new window.google.maps.LatLng(lat, lng);
- map.setCenter(newCenter);
- map.setZoom(13);
- console.log(`Map centered on: ${lat}, ${lng}`);
- } else {
- console.error("Map not initialized");
- }
- }
-
- props.setCenterOnLocation(centerOnLocation);
-
+export default function Map(props) {
+ const { apikey } = props.logData;
+ const stableSetCenterOnLocation = useCallback(props.setCenterOnLocation, []);
return (
-
+
);
}
-
-// Add a new function to find the closest event to a clicked location
-function findClosestEvent(clickLocation, maxDistance) {
- const logs = tripLogs.getLogs_(minDate, maxDate).value();
- let closestEvent = null;
- let closestDistance = maxDistance;
-
- logs.forEach((event) => {
- const rawLocation = _.get(event, "lastlocation.rawlocation");
- if (rawLocation && rawLocation.latitude && rawLocation.longitude) {
- const eventLocation = new window.google.maps.LatLng(rawLocation.latitude, rawLocation.longitude);
- const distance = window.google.maps.geometry.spherical.computeDistanceBetween(clickLocation, eventLocation);
-
- if (distance < closestDistance) {
- closestEvent = event;
- closestDistance = distance;
- }
- }
- });
-
- if (closestEvent) {
- log("Found closest event at distance:", closestDistance, "meters");
- } else {
- log("No events found within", maxDistance, "meters");
- }
-
- return closestEvent;
-}
-
-/*
- * GenerateBubbles() -- helper function for generating map features based
- * on per-log entry data.
- *
- * Handles the gunk of iterating over log entries and clearing/setting the map
- */
-function GenerateBubbles(bubbleName, cb) {
- return (showBubble) => {
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
- if (showBubble) {
- bubbleMap[bubbleName] = tripLogs
- .getLogs_(minDate, maxDate)
- .map((le) => {
- const lastLocation = le.lastlocation;
- let rawlocation;
- let bubble = undefined;
- if (lastLocation && (rawlocation = lastLocation.rawlocation)) {
- bubble = cb(
- new window.google.maps.LatLng({
- lat: rawlocation.latitude,
- lng: rawlocation.longitude,
- }),
- lastLocation,
- le
- );
- }
- return bubble;
- })
- .compact()
- .value();
- }
- };
-}
-
-/*
- * Draws circles on map with a radius equal to the
- * GPS accuracy.
- */
-toggleHandlers["showGPSBubbles"] = GenerateBubbles("showGPSBubbles", (rawLocationLatLng, lastLocation) => {
- let color;
- switch (lastLocation.locationsensor) {
- case "GPS":
- color = "#11FF11";
- break;
- case "NETWORK":
- color = "#FF1111";
- break;
- case "PASSIVE":
- color = "#FF0000";
- break;
- case "ROAD_SNAPPED_LOCATION_PROVIDER":
- color = "#00FF00";
- break;
- case "FUSED_LOCATION_PROVIDER":
- color = "#11FF11";
- break;
- case "LOG_UNSPECIFIED":
- default:
- color = "#000000";
- }
- const accuracy = lastLocation.rawlocationaccuracy;
- if (accuracy) {
- let circ = new window.google.maps.Circle({
- strokeColor: color,
- strokeOpacity: 0.6,
- strokeWeight: 2,
- fillColor: color,
- fillOpacity: 0.2,
- map,
- center: rawLocationLatLng,
- radius: accuracy, // units is this actually meters?
- });
- window.google.maps.event.addListener(circ, "mouseover", () => {
- setFeaturedObject({
- rawlocationaccuracy: lastLocation.rawlocationaccuracy,
- locationsensor: lastLocation.locationsensor,
- });
- });
- return circ;
- }
-});
-
-/*
- * Draws circles on map with a radius equal to the
- * time delta (1 meter radius = 1 second of delta)
- */
-toggleHandlers["showClientServerTimeDeltas"] = GenerateBubbles(
- "showClientServerTimeDeltas",
- (rawLocationLatLng, lastLocation, logEntry) => {
- const clientTimeStr = _.get(logEntry.lastlocationResponse, "rawlocationtime");
- const serverTimeStr = _.get(logEntry.lastlocationResponse, "servertime");
- if (clientTimeStr && serverTimeStr) {
- const clientDate = new Date(clientTimeStr);
- const serverDate = new Date(serverTimeStr);
- const timeDeltaSeconds = Math.abs(clientDate.getTime() - serverDate.getTime()) / 1000;
- let color;
- if (clientDate > serverDate) {
- color = "#0000F0";
- } else {
- color = "#0F0000";
- }
-
- let circ = new window.google.maps.Circle({
- strokeColor: color,
- strokeOpacity: 0.6,
- strokeWeight: 2,
- fillColor: color,
- fillOpacity: 0.2,
- map,
- center: rawLocationLatLng,
- radius: timeDeltaSeconds,
- });
- window.google.maps.event.addListener(circ, "mouseover", () => {
- setFeaturedObject({
- timeDeltaSeconds: timeDeltaSeconds,
- serverDate: serverDate,
- clientDate: clientDate,
- });
- });
- return circ;
- }
- }
-);
-
-/*
- * Draws arrows on map showing the measured heading
- * of the vehicle (ie which direction vehicle was traveling
- */
-toggleHandlers["showHeading"] = GenerateBubbles("showHeading", (rawLocationLatLng, lastLocation, logEntry) => {
- // Note: Heading & accuracy are only on the _request_ not the
- // response.
- const heading = _.get(logEntry.lastlocation, "heading");
- const accuracy = _.get(logEntry.lastlocation, "headingaccuracy");
-
- // Not currently using accuracy. How to show it? Maybe opacity of the arrorw?
- const arrowLength = 20; // meters??
- if (!(heading && accuracy)) {
- return;
- }
- const headingLine = new window.google.maps.Polyline({
- strokeColor: "#0000F0",
- strokeOpacity: 0.6,
- strokeWeight: 2,
- icons: [
- {
- icon: {
- path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
- strokeColor: "#0000FF",
- strokeWeight: 4,
- },
- offset: "100%",
- },
- ],
- map,
- path: [
- rawLocationLatLng,
- window.google.maps.geometry.spherical.computeOffset(rawLocationLatLng, arrowLength, heading),
- ],
- });
- window.google.maps.event.addListener(headingLine, "click", () => {
- // TODO: allow updating panorama based on forward/back
- // stepper buttons (ie at each updatevehicle log we have a heading)
- panorama = new window.google.maps.StreetViewPanorama(document.getElementById("map"), {
- position: rawLocationLatLng,
- pov: { heading: heading, pitch: 10 },
- addressControlOptions: {
- position: window.google.maps.ControlPosition.BOTTOM_CENTER,
- },
- linksControl: false,
- panControl: false,
- enableCloseButton: true,
- });
- console.log("loaded panorama", panorama);
- });
- return headingLine;
-});
-
-/*
- * Draws circles on the map. Color indicates vehicle speed at that
- * location.
- */
-toggleHandlers["showSpeed"] = GenerateBubbles("showSpeed", (rawLocationLatLng, lastLocation) => {
- const speed = lastLocation.speed;
- if (lastLocation.speed === undefined) {
- return;
- }
- const color = speed < 0 ? "#FF0000" : "#00FF00";
- return new window.google.maps.Circle({
- strokeColor: color,
- strokeOpacity: 0.5,
- fillColor: color,
- fillOpacity: 0.5,
- map,
- center: rawLocationLatLng,
- radius: Math.abs(speed),
- });
-});
-
-/*
- * Draws circles on the map. Color indicates trip status
- * at that location. Note that trip status isn't actually
- * in the update vehicle logs, so current trip status will actually
- * just be the trip status at the time of the vehicle update --
- * which is a bit wrong and wonky on the boundaries.
- */
-toggleHandlers["showTripStatus"] = GenerateBubbles("showTripStatus", (rawLocationLatLng, lastLocation, le) => {
- let color,
- radius = 5;
- const tripStatus = tripLogs.getTripStatusAtDate(le.date);
- switch (tripStatus) {
- case "NEW":
- color = "#002200";
- radius = 30;
- break;
- case "ENROUTE_TO_PICKUP":
- color = "#FFFF00";
- break;
- case "ARRIVED_AT_PICKUP":
- color = "#FFFF10";
- radius = 10;
- break;
- case "ARRIVED_AT_INTERMEDIATE_DESTINATION":
- color = "#10FFFF";
- radius = 20;
- break;
- case "ENROUTE_TO_DROPOFF":
- color = "#00FFFF";
- break;
- case "COMPLETE":
- radius = 30;
- color = "#00FF00";
- break;
- case "CANCELED":
- radius = 30;
- color = "#FF0000";
- break;
- case "UNKNOWN_TRIP_STATUS":
- default:
- color = "#000000";
- }
-
- const statusCirc = new window.google.maps.Circle({
- strokeColor: color,
- strokeOpacity: 0.5,
- fillColor: color,
- fillOpacity: 0.5,
- map,
- center: rawLocationLatLng,
- radius: radius, // set based on trip status?
- });
- window.google.maps.event.addListener(statusCirc, "mouseover", () => {
- setFeaturedObject({
- tripStatus: tripStatus,
- });
- });
- return statusCirc;
-});
-
-/*
- * Enable/disables live traffic layer
- */
-toggleHandlers["showTraffic"] = function (enabled) {
- if (!trafficLayer) {
- trafficLayer = new window.google.maps.TrafficLayer();
- }
- if (enabled) {
- trafficLayer.setMap(map);
- } else {
- trafficLayer.setMap(null);
- }
-};
-
-/*
- * Draws circles on the map. Size indicates dwell time at that
- * location.
- */
-toggleHandlers["showDwellLocations"] = function (enabled) {
- const bubbleName = "showDwellLocations";
- const dwellLocations = tripLogs.getDwellLocations(minDate, maxDate);
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
- if (enabled) {
- bubbleMap[bubbleName] = _.map(dwellLocations, (dl) => {
- const circ = new window.google.maps.Circle({
- strokeColor: "#000000",
- strokeOpacity: 0.25,
- fillColor: "#FFFF00",
- fillOpacity: 0.25,
- map,
- center: dl.leaderCoords,
- radius: dl.updates * 3, // make dwell times more obvious
- });
- window.google.maps.event.addListener(circ, "mouseover", () => {
- setFeaturedObject({
- startDate: dl.startDate,
- duration: Utils.formatDuration(dl.endDate - dl.startDate),
- endDate: dl.endDate,
- });
- });
- return circ;
- });
- }
-};
-
-/*
- * 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 window.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}`,
- });
- window.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: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
- strokeColor: arrowColor,
- strokeWeight: 4,
- },
- offset: "100%",
- },
- ],
- });
- ret.push(offSetPath);
- }
- return ret;
- })
- .flatten()
- .value();
- }
-};
-
-/*
- * Draws planned paths on the map.
- */
-toggleHandlers["showPlannedPaths"] = function (enabled) {
- const bubbleName = "showPlannedPaths";
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
-
- if (enabled) {
- const trips = tripLogs.getTrips();
- bubbleMap[bubbleName] = trips
- .filter((trip) => {
- return trip.firstUpdate <= maxDate && trip.lastUpdate >= minDate && trip.getPlannedPath().length > 0;
- })
- .map((trip) => {
- const plannedPath = trip.getPlannedPath();
- const path = new window.google.maps.Polyline({
- path: plannedPath,
- geodesic: true,
- strokeColor: getColor(trip.tripIdx),
- strokeOpacity: 0.3,
- strokeWeight: 6,
- });
- path.setMap(map);
- return path;
- });
- }
-};
-
-/*
- * Draws circles on the map. Size indicates dwell time at that
- * location.
- */
-toggleHandlers["showNavStatus"] = GenerateBubbles("showNavStatus", (rawLocationLatLng, lastLocation, le) => {
- const navStatus = le.navStatus;
- if (navStatus === undefined) {
- return;
- }
- let color,
- radius = 5;
- switch (navStatus) {
- case "UNKNOWN_NAVIGATION_STATUS":
- color = "#222222";
- break;
- case "NO_GUIDANCE":
- color = "#090909";
- break;
- case "ENROUTE_TO_DESTINATION":
- color = "#00FF00";
- break;
- case "OFF_ROUTE":
- color = "#FF0000";
- radius = 30;
- break;
- case "ARRIVED_AT_DESTINATION":
- color = "0000FF";
- radius = 10;
- break;
- default:
- color = "#000000";
- }
- const statusCirc = new window.google.maps.Circle({
- strokeColor: color,
- strokeOpacity: 0.5,
- fillColor: color,
- fillOpacity: 0.5,
- map,
- center: rawLocationLatLng,
- radius: radius, // set based on trip status?
- });
- window.google.maps.event.addListener(statusCirc, "mouseover", () => {
- setFeaturedObject({
- navStatus: navStatus,
- vehicleState: _.get(le, "response.vehiclestate"),
- tripStatus: "??",
- });
- });
- return statusCirc;
-});
-
-/*
- * Draws circles on the map. Size indicates delta in seconds at that
- * location.
- */
-toggleHandlers["showETADeltas"] = function (enabled) {
- const bubbleName = "showETADeltas";
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
- const etaDeltas = tripLogs.getETADeltas(minDate, maxDate);
- if (enabled) {
- bubbleMap[bubbleName] = _.map(etaDeltas, (etaDelta) => {
- const circ = new window.google.maps.Circle({
- strokeColor: "#000000",
- strokeOpacity: 0.25,
- fillColor: "FF0000",
- fillOpacity: 0.25,
- map,
- center: etaDelta.coords,
- // cap radius to 300 meters to avoid coloring the whole
- // screen when there is a very large delta. Definitely
- // needs tuning ... and likely better to consider adjusting
- // color as well.
- radius: _.min([etaDelta.deltaInSeconds, 300]),
- });
- window.google.maps.event.addListener(circ, "mouseover", () => {
- setFeaturedObject({
- etaDeltaInSeconds: etaDelta.deltaInSeconds,
- });
- });
- return circ;
- });
- }
-};
-
-/*
- * Draws arrows on the map showing where a vehicle jumped
- * from one location to another at an unrealistic velocity.
- */
-toggleHandlers["showHighVelocityJumps"] = function (enabled) {
- const bubbleName = "showHighVelocityJumps";
-
- tripLogs.debouncedGetHighVelocityJumps(minDate, maxDate, (jumps) => {
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
-
- if (enabled) {
- bubbleMap[bubbleName] = _(jumps)
- .map((jump) => {
- function getStrokeWeight(velocity) {
- if (velocity <= 100) {
- return 2;
- } else if (velocity < 1000) {
- return 6;
- } else if (velocity < 2000) {
- return 10;
- } else {
- return 14;
- }
- }
- const path = new window.google.maps.Polyline({
- path: [jump.startLoc, jump.endLoc],
- geodesic: true,
- strokeColor: getColor(jump.jumpIdx),
- strokeOpacity: 0.8,
- strokeWeight: getStrokeWeight(jump.velocity),
- map: map,
- icons: [
- {
- icon: {
- path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
- strokeColor: getColor(jump.jumpIdx),
- strokeWeight: getStrokeWeight(jump.velocity),
- },
- offset: "100%",
- },
- ],
- });
- window.google.maps.event.addListener(path, "mouseover", () => {
- setFeaturedObject(jump.getFeaturedData());
- });
- window.google.maps.event.addListener(path, "click", () => {
- setFeaturedObject(jump.getFeaturedData());
- // show a minute +/- on each side of a jump
- setTimeRange(jump.startDate.getTime() - 60 * 1000, jump.endDate.getTime() + 60 * 1000);
- });
- return [path];
- })
- .flatten()
- .value();
- } else {
- console.log("Disabling high velocity jumps");
- setTimeRange(tripLogs.minDate.getTime(), tripLogs.maxDate.getTime());
- }
- });
-};
-
-/*
- * Marks locations on the map where we did not get the expected
- * updateVehicle requests
- */
-toggleHandlers["showMissingUpdates"] = function (enabled) {
- const bubbleName = "showMissingUpdates";
- const missingUpdates = tripLogs.getMissingUpdates(minDate, maxDate);
- _.forEach(bubbleMap[bubbleName], (bubble) => bubble.setMap(null));
- delete bubbleMap[bubbleName];
- if (enabled) {
- bubbleMap[bubbleName] = _(missingUpdates)
- .map((update) => {
- function getStrokeWeight(interval) {
- if (interval <= 60 * 1000) {
- return 2;
- } else if (interval < 60 * 10 * 1000) {
- return 6;
- } else if (interval < 60 * 60 * 10 * 1000) {
- return 10;
- } else {
- return 14;
- }
- }
- const heading = window.google.maps.geometry.spherical.computeHeading(update.startLoc, update.endLoc);
- const offsetHeading = ((heading + 360 + 90) % 360) - 180;
- const points = [
- update.startLoc,
- window.google.maps.geometry.spherical.computeOffset(
- update.startLoc,
- 1000, //TODO compute based on viewport?
- offsetHeading
- ),
- window.google.maps.geometry.spherical.computeOffset(
- update.startLoc,
- 900, //TODO compute based on viewport?
- offsetHeading
- ),
- window.google.maps.geometry.spherical.computeOffset(
- update.endLoc,
- 900, //TODO compute based on viewport?
- offsetHeading
- ),
- window.google.maps.geometry.spherical.computeOffset(
- update.endLoc,
- 1000, //TODO compute based on viewport?
- offsetHeading
- ),
- update.endLoc,
- ];
- const path = new window.google.maps.Polyline({
- path: points,
- geodesic: true,
- strokeColor: "#008B8B",
- strokeOpacity: 0.5,
- strokeWeight: getStrokeWeight(update.interval),
- map: map,
- icons: [
- {
- icon: {
- path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
- strokeColor: "#008B8B",
- strokeWeight: getStrokeWeight(update.interval),
- scale: 6,
- },
- offset: "50%",
- },
- {
- icon: {
- path: window.google.maps.SymbolPath.CIRCLE,
- scale: 6,
- strokeColor: "#000000",
- strokeWeight: 1,
- strokeOpacity: 0.5,
- },
- offset: "0%",
- },
- {
- icon: {
- path: window.google.maps.SymbolPath.CIRCLE,
- scale: 6,
- strokeColor: "#000000",
- strokeWeight: 1,
- strokeOpacity: 0.5,
- },
- offset: "100%",
- },
- ],
- });
- window.google.maps.event.addListener(path, "mouseover", () => {
- setFeaturedObject(update.getFeaturedData());
- path.setOptions({
- strokeOpacity: 1,
- strokeWeight: 1.5 * getStrokeWeight(update.interval),
- });
- });
- window.google.maps.event.addListener(path, "mouseout", () => {
- path.setOptions({
- strokeOpacity: 0.5,
- strokeWeight: getStrokeWeight(update.interval),
- });
- });
- window.google.maps.event.addListener(path, "click", () => {
- setFeaturedObject(update.getFeaturedData());
- // show a minute +/- on each side of a update
- setTimeRange(update.startDate.getTime() - 60 * 1000, update.endDate.getTime() + 60 * 1000);
- });
- return [path];
- })
- .flatten()
- .value();
- } else {
- // TODO: ideally reset to timerange that was selected before enabling
- // jump view
- setTimeRange(tripLogs.minDate.getTime(), tripLogs.maxDate.getTime());
- }
-};
-
-/*
- * Enable/disables live journey sharing view
- */
-toggleHandlers["showLiveJS"] = function (enabled) {
- if (!jwt) {
- console.log("Issue #25 -- no/invalid jwt");
- return;
- }
- // call into js to set the trip
- if (enabled) {
- locationProvider.tripId = _.last(tripLogs.getTripIDs());
- } else {
- locationProvider.tripId = "";
- }
-};
-
-export { Map as default };
diff --git a/src/MapToggles.js b/src/MapToggles.js
new file mode 100644
index 0000000..ae99d0a
--- /dev/null
+++ b/src/MapToggles.js
@@ -0,0 +1,590 @@
+// src/MapToggles.js
+import _ from "lodash";
+import { getColor } from "./Trip";
+import Utils, { log } from "./Utils";
+
+export const ALL_TOGGLES = [
+ {
+ id: "showGPSBubbles",
+ name: "GPS Accuracy",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/GPSAccuracy.md",
+ columns: ["lastlocation.rawlocationaccuracy", "lastlocation.locationsensor"],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showHeading",
+ name: "Heading",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Heading.md",
+ columns: ["lastlocation.heading", "lastlocation.headingaccuracy"],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showSpeed",
+ name: "Speed",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Speed.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showTripStatus",
+ name: "Trip Status",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/TripStatus.md",
+ columns: [],
+ solutionTypes: ["ODRD"],
+ },
+ {
+ id: "showNavStatus",
+ name: "Navigation Status",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/NavStatus.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showTasksAsCreated",
+ name: "Tasks",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/Tasks.md",
+ columns: [],
+ solutionTypes: ["LMFS"],
+ },
+ {
+ id: "showPlannedPaths",
+ name: "Planned Paths",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/PlannedPaths.md",
+ columns: [],
+ solutionTypes: ["LMFS"],
+ },
+ {
+ id: "showDwellLocations",
+ name: "Dwell Locations",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/DwellTimes.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showHighVelocityJumps",
+ name: "Jumps (unrealistic velocity)",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/VelocityJumps.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showMissingUpdates",
+ name: "Jumps (Temporal)",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/MissingUpdates.md",
+ columns: ["temporal_gap"],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showClientServerTimeDeltas",
+ name: "Client/Server Time Deltas",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showETADeltas",
+ name: "ETA Deltas",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/docs/EtaDeltas.md",
+ columns: ["request.vehicle.etatofirstwaypoint"],
+ solutionTypes: ["ODRD"],
+ },
+ {
+ id: "showTraffic",
+ name: "Live Traffic",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+ {
+ id: "showLiveJS",
+ name: "Live Journey Sharing",
+ docLink: "https://github.com/googlemaps/fleet-debugger/blob/main/README.md",
+ columns: [],
+ solutionTypes: ["ODRD", "LMFS"],
+ },
+];
+
+export function getVisibleToggles(solutionType) {
+ if (!solutionType) return [];
+ log(`Filtering toggles for solution type: ${solutionType}`);
+ return _.filter(ALL_TOGGLES, (toggle) => toggle.solutionTypes.indexOf(solutionType) !== -1);
+}
+
+export function getToggleHandlers({
+ map,
+ tripLogs,
+ taskLogs,
+ minDate,
+ maxDate,
+ setFeaturedObject,
+ setTimeRange,
+ bubbleMapRef,
+ panoramaRef,
+ mapDivRef,
+ trafficLayerRef,
+ locationProviderRef,
+ jwt,
+}) {
+ const GenerateBubbles = (bubbleName, cb) => (showBubble) => {
+ _.forEach(bubbleMapRef.current[bubbleName], (bubble) => bubble.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (showBubble) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = tripLogs
+ .getLogs_(minDate, maxDate)
+ .map((le) => {
+ const rawloc = le.lastlocation?.rawlocation;
+ if (rawloc && typeof rawloc.latitude === "number" && typeof rawloc.longitude === "number") {
+ const latLng = new window.google.maps.LatLng(rawloc.latitude, rawloc.longitude);
+ return cb(latLng, le.lastlocation, le);
+ }
+ return null;
+ })
+ .compact()
+ .value();
+ }
+ };
+
+ return {
+ showGPSBubbles: GenerateBubbles("showGPSBubbles", (rawLocationLatLng, lastLocation) => {
+ let color;
+ switch (lastLocation.locationsensor) {
+ case "GPS":
+ color = "#11FF11";
+ break;
+ case "NETWORK":
+ color = "#FF1111";
+ break;
+ case "PASSIVE":
+ color = "#FF0000";
+ break;
+ case "ROAD_SNAPPED_LOCATION_PROVIDER":
+ color = "#00FF00";
+ break;
+ case "FUSED_LOCATION_PROVIDER":
+ color = "#11FF11";
+ break;
+ case "LOG_UNSPECIFIED":
+ default:
+ color = "#000000";
+ }
+ const accuracy = lastLocation.rawlocationaccuracy;
+ if (accuracy) {
+ const circ = new window.google.maps.Circle({
+ strokeColor: color,
+ strokeOpacity: 0.6,
+ strokeWeight: 2,
+ fillColor: color,
+ fillOpacity: 0.2,
+ map,
+ center: rawLocationLatLng,
+ radius: accuracy,
+ });
+ window.google.maps.event.addListener(circ, "mouseover", () => {
+ setFeaturedObject({
+ rawlocationaccuracy: lastLocation.rawlocationaccuracy,
+ locationsensor: lastLocation.locationsensor,
+ });
+ });
+ return circ;
+ }
+ }),
+ showClientServerTimeDeltas: GenerateBubbles(
+ "showClientServerTimeDeltas",
+ (rawLocationLatLng, lastLocation, logEntry) => {
+ const clientTimeStr = _.get(logEntry.lastlocationResponse, "rawlocationtime");
+ const serverTimeStr = _.get(logEntry.lastlocationResponse, "servertime");
+ if (clientTimeStr && serverTimeStr) {
+ const clientDate = new Date(clientTimeStr);
+ const serverDate = new Date(serverTimeStr);
+ const timeDeltaSeconds = Math.abs(clientDate.getTime() - serverDate.getTime()) / 1000;
+ let color = clientDate > serverDate ? "#0000F0" : "#0F0000";
+
+ const circ = new window.google.maps.Circle({
+ strokeColor: color,
+ strokeOpacity: 0.6,
+ strokeWeight: 2,
+ fillColor: color,
+ fillOpacity: 0.2,
+ map,
+ center: rawLocationLatLng,
+ radius: timeDeltaSeconds,
+ });
+ window.google.maps.event.addListener(circ, "mouseover", () => {
+ setFeaturedObject({
+ timeDeltaSeconds: timeDeltaSeconds,
+ serverDate: serverDate,
+ clientDate: clientDate,
+ });
+ });
+ return circ;
+ }
+ }
+ ),
+ showHeading: GenerateBubbles("showHeading", (rawLocationLatLng, lastLocation, logEntry) => {
+ const heading = _.get(logEntry.lastlocation, "heading");
+ const accuracy = _.get(logEntry.lastlocation, "headingaccuracy");
+ if (heading === undefined || accuracy === undefined) {
+ return;
+ }
+ const headingLine = new window.google.maps.Polyline({
+ strokeColor: "#0000F0",
+ strokeOpacity: 0.6,
+ strokeWeight: 2,
+ icons: [
+ {
+ icon: {
+ path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
+ strokeColor: "#0000FF",
+ strokeWeight: 4,
+ },
+ offset: "100%",
+ },
+ ],
+ map,
+ path: [rawLocationLatLng, window.google.maps.geometry.spherical.computeOffset(rawLocationLatLng, 20, heading)],
+ });
+ headingLine.addListener("click", () => {
+ log("Heading line clicked, showing StreetView.");
+ panoramaRef.current = new window.google.maps.StreetViewPanorama(mapDivRef.current, {
+ position: rawLocationLatLng,
+ pov: { heading, pitch: 10 },
+ });
+ });
+ return headingLine;
+ }),
+ showSpeed: GenerateBubbles("showSpeed", (rawLocationLatLng, lastLocation) => {
+ const speed = lastLocation.speed;
+ if (speed === undefined) {
+ return;
+ }
+ const color = speed < 0 ? "#FF0000" : "#00FF00";
+ return new window.google.maps.Circle({
+ strokeColor: color,
+ strokeOpacity: 0.5,
+ fillColor: color,
+ fillOpacity: 0.5,
+ map,
+ center: rawLocationLatLng,
+ radius: Math.abs(speed),
+ });
+ }),
+ showTripStatus: GenerateBubbles("showTripStatus", (rawLocationLatLng, lastLocation, le) => {
+ let color,
+ radius = 5;
+ const tripStatus = tripLogs.getTripStatusAtDate(le.date);
+ switch (tripStatus) {
+ case "NEW":
+ color = "#002200";
+ radius = 30;
+ break;
+ case "ENROUTE_TO_PICKUP":
+ color = "#FFFF00";
+ break;
+ case "ARRIVED_AT_PICKUP":
+ color = "#FFFF10";
+ radius = 10;
+ break;
+ case "ARRIVED_AT_INTERMEDIATE_DESTINATION":
+ color = "#10FFFF";
+ radius = 20;
+ break;
+ case "ENROUTE_TO_DROPOFF":
+ color = "#00FFFF";
+ break;
+ case "COMPLETE":
+ radius = 30;
+ color = "#00FF00";
+ break;
+ case "CANCELED":
+ radius = 30;
+ color = "#FF0000";
+ break;
+ case "UNKNOWN_TRIP_STATUS":
+ default:
+ color = "#000000";
+ }
+
+ const statusCirc = new window.google.maps.Circle({
+ strokeColor: color,
+ strokeOpacity: 0.5,
+ fillColor: color,
+ fillOpacity: 0.5,
+ map,
+ center: rawLocationLatLng,
+ radius: radius,
+ });
+ window.google.maps.event.addListener(statusCirc, "mouseover", () => {
+ setFeaturedObject({
+ tripStatus: tripStatus,
+ });
+ });
+ return statusCirc;
+ }),
+ showNavStatus: GenerateBubbles("showNavStatus", (rawLocationLatLng, lastLocation, le) => {
+ const navStatus = le.navStatus;
+ if (navStatus === undefined) {
+ return;
+ }
+ let color,
+ radius = 5;
+ switch (navStatus) {
+ case "UNKNOWN_NAVIGATION_STATUS":
+ color = "#222222";
+ break;
+ case "NO_GUIDANCE":
+ color = "#090909";
+ break;
+ case "ENROUTE_TO_DESTINATION":
+ color = "#00FF00";
+ break;
+ case "OFF_ROUTE":
+ color = "#FF0000";
+ radius = 30;
+ break;
+ case "ARRIVED_AT_DESTINATION":
+ color = "#0000FF";
+ radius = 10;
+ break;
+ default:
+ color = "#000000";
+ }
+ const statusCirc = new window.google.maps.Circle({
+ strokeColor: color,
+ strokeOpacity: 0.5,
+ fillColor: color,
+ fillOpacity: 0.5,
+ map,
+ center: rawLocationLatLng,
+ radius: radius,
+ });
+ window.google.maps.event.addListener(statusCirc, "mouseover", () => {
+ setFeaturedObject({
+ navStatus: navStatus,
+ });
+ });
+ return statusCirc;
+ }),
+ showTraffic: (enabled) => {
+ if (!trafficLayerRef.current) {
+ trafficLayerRef.current = new window.google.maps.TrafficLayer();
+ }
+ if (enabled) {
+ log("Enabling Traffic");
+ trafficLayerRef.current.setMap(map);
+ } else {
+ log("Disabling Traffic");
+ trafficLayerRef.current.setMap(null);
+ }
+ },
+ showLiveJS: (enabled) => {
+ if (!jwt) {
+ log("Cannot enable LiveJS: JWT is missing.");
+ return;
+ }
+ if (enabled) {
+ log("Enabling LiveJS");
+ locationProviderRef.current.tripId = _.last(tripLogs.getTripIDs());
+ } else {
+ log("Disabling LiveJS");
+ locationProviderRef.current.tripId = "";
+ }
+ },
+ showDwellLocations: (enabled) => {
+ const bubbleName = "showDwellLocations";
+ _.forEach(bubbleMapRef.current[bubbleName], (bubble) => bubble.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = _.map(tripLogs.getDwellLocations(minDate, maxDate), (dl) => {
+ const circ = new window.google.maps.Circle({
+ strokeColor: "#000000",
+ strokeOpacity: 0.25,
+ fillColor: "#FFFF00",
+ fillOpacity: 0.25,
+ map,
+ center: dl.leaderCoords,
+ radius: dl.updates * 3,
+ });
+ window.google.maps.event.addListener(circ, "mouseover", () => {
+ setFeaturedObject({
+ startDate: dl.startDate,
+ duration: Utils.formatDuration(dl.endDate - dl.startDate),
+ endDate: dl.endDate,
+ });
+ });
+ return circ;
+ });
+ }
+ },
+ showTasksAsCreated: (enabled) => {
+ const bubbleName = "showTasksAsCreated";
+ _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = _.map(
+ taskLogs.getTasks(maxDate).value(),
+ (t) => new window.google.maps.Marker({ map, position: t.plannedlocation.point })
+ );
+ }
+ },
+ showPlannedPaths: (enabled) => {
+ const bubbleName = "showPlannedPaths";
+ _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = tripLogs
+ .getTrips()
+ .filter((t) => t.getPlannedPath().length > 0)
+ .map(
+ (t) => new window.google.maps.Polyline({ map, path: t.getPlannedPath(), strokeColor: getColor(t.tripIdx) })
+ );
+ }
+ },
+ showETADeltas: (enabled) => {
+ const bubbleName = "showETADeltas";
+ _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = _.map(tripLogs.getETADeltas(minDate, maxDate), (d) => {
+ const circ = new window.google.maps.Circle({
+ map,
+ center: d.coords,
+ radius: _.min([d.deltaInSeconds, 300]),
+ strokeColor: "#000000",
+ strokeOpacity: 0.25,
+ fillColor: "#FF0000",
+ fillOpacity: 0.25,
+ });
+ window.google.maps.event.addListener(circ, "mouseover", () => {
+ setFeaturedObject({
+ etaDeltaInSeconds: d.deltaInSeconds,
+ });
+ });
+ return circ;
+ });
+ }
+ },
+ showHighVelocityJumps: (enabled) => {
+ const bubbleName = "showHighVelocityJumps";
+ tripLogs.debouncedGetHighVelocityJumps(minDate, maxDate, (jumps) => {
+ _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = _(jumps)
+ .map((jump) => {
+ const getStrokeWeight = (velocity) =>
+ velocity <= 100 ? 2 : velocity < 1000 ? 6 : velocity < 2000 ? 10 : 14;
+ const path = new window.google.maps.Polyline({
+ path: [jump.startLoc, jump.endLoc],
+ geodesic: true,
+ strokeColor: getColor(jump.jumpIdx),
+ strokeOpacity: 0.8,
+ strokeWeight: getStrokeWeight(jump.velocity),
+ map: map,
+ icons: [
+ {
+ icon: {
+ path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
+ strokeColor: getColor(jump.jumpIdx),
+ strokeWeight: getStrokeWeight(jump.velocity),
+ },
+ offset: "100%",
+ },
+ ],
+ });
+ path.addListener("mouseover", () => setFeaturedObject(jump.getFeaturedData()));
+ path.addListener("click", () => {
+ setFeaturedObject(jump.getFeaturedData());
+ setTimeRange(jump.startDate.getTime() - 60 * 1000, jump.endDate.getTime() + 60 * 1000);
+ });
+ return [path];
+ })
+ .flatten()
+ .value();
+ }
+ });
+ },
+ showMissingUpdates: (enabled) => {
+ const bubbleName = "showMissingUpdates";
+ _.forEach(bubbleMapRef.current[bubbleName], (b) => b.setMap(null));
+ delete bubbleMapRef.current[bubbleName];
+ if (enabled) {
+ log(`Enabling ${bubbleName}`);
+ bubbleMapRef.current[bubbleName] = _(tripLogs.getMissingUpdates(minDate, maxDate))
+ .map((update) => {
+ const getStrokeWeight = (interval) =>
+ interval <= 60000 ? 2 : interval < 600000 ? 6 : interval < 36000000 ? 10 : 14;
+ const heading = window.google.maps.geometry.spherical.computeHeading(update.startLoc, update.endLoc);
+ const offsetHeading = ((heading + 360 + 90) % 360) - 180;
+ const points = [
+ update.startLoc,
+ window.google.maps.geometry.spherical.computeOffset(update.startLoc, 1000, offsetHeading),
+ window.google.maps.geometry.spherical.computeOffset(update.startLoc, 900, offsetHeading),
+ window.google.maps.geometry.spherical.computeOffset(update.endLoc, 900, offsetHeading),
+ window.google.maps.geometry.spherical.computeOffset(update.endLoc, 1000, offsetHeading),
+ update.endLoc,
+ ];
+ const path = new window.google.maps.Polyline({
+ path: points,
+ geodesic: true,
+ strokeColor: "#008B8B",
+ strokeOpacity: 0.5,
+ strokeWeight: getStrokeWeight(update.interval),
+ map: map,
+ icons: [
+ {
+ icon: {
+ path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
+ strokeColor: "#008B8B",
+ strokeWeight: getStrokeWeight(update.interval),
+ scale: 6,
+ },
+ offset: "50%",
+ },
+ {
+ icon: {
+ path: window.google.maps.SymbolPath.CIRCLE,
+ scale: 6,
+ strokeColor: "#000000",
+ strokeWeight: 1,
+ strokeOpacity: 0.5,
+ },
+ offset: "0%",
+ },
+ {
+ icon: {
+ path: window.google.maps.SymbolPath.CIRCLE,
+ scale: 6,
+ strokeColor: "#000000",
+ strokeWeight: 1,
+ strokeOpacity: 0.5,
+ },
+ offset: "100%",
+ },
+ ],
+ });
+ path.addListener("mouseover", () => {
+ setFeaturedObject(update.getFeaturedData());
+ path.setOptions({ strokeOpacity: 1, strokeWeight: 1.5 * getStrokeWeight(update.interval) });
+ });
+ path.addListener("mouseout", () =>
+ path.setOptions({ strokeOpacity: 0.5, strokeWeight: getStrokeWeight(update.interval) })
+ );
+ path.addListener("click", () => {
+ setFeaturedObject(update.getFeaturedData());
+ setTimeRange(update.startDate.getTime() - 60 * 1000, update.endDate.getTime() + 60 * 1000);
+ });
+ return [path];
+ })
+ .flatten()
+ .value();
+ }
+ },
+ };
+}
diff --git a/src/MapToggles.test.js b/src/MapToggles.test.js
new file mode 100644
index 0000000..1567dc3
--- /dev/null
+++ b/src/MapToggles.test.js
@@ -0,0 +1,51 @@
+// src/MapToggles.test.js
+import { getVisibleToggles, ALL_TOGGLES } from "./MapToggles";
+
+describe("getVisibleToggles", () => {
+ test('should return ODRD and common toggles for "ODRD" solution type', () => {
+ const solutionType = "ODRD";
+ const visibleToggles = getVisibleToggles(solutionType);
+
+ // Manually check for a toggle that is ODRD-only
+ expect(visibleToggles.some((t) => t.id === "showTripStatus")).toBe(true);
+ // Manually check for a toggle that is NOT for ODRD
+ expect(visibleToggles.some((t) => t.id === "showTasksAsCreated")).toBe(false);
+
+ // Compare against the master list for accuracy
+ const expectedToggles = ALL_TOGGLES.filter((t) => t.solutionTypes.includes("ODRD"));
+ expect(visibleToggles).toHaveLength(expectedToggles.length);
+
+ const visibleToggleIds = visibleToggles.map((t) => t.id);
+ const expectedToggleIds = expectedToggles.map((t) => t.id);
+ expect(visibleToggleIds).toEqual(expect.arrayContaining(expectedToggleIds));
+ });
+
+ test('should return LMFS and common toggles for "LMFS" solution type', () => {
+ const solutionType = "LMFS";
+ const visibleToggles = getVisibleToggles(solutionType);
+
+ // Manually check for a toggle that is LMFS-only
+ expect(visibleToggles.some((t) => t.id === "showTasksAsCreated")).toBe(true);
+ // Manually check for a toggle that is NOT for LMFS
+ expect(visibleToggles.some((t) => t.id === "showTripStatus")).toBe(false);
+
+ const expectedToggles = ALL_TOGGLES.filter((t) => t.solutionTypes.includes("LMFS"));
+ expect(visibleToggles).toHaveLength(expectedToggles.length);
+
+ const visibleToggleIds = visibleToggles.map((t) => t.id);
+ const expectedToggleIds = expectedToggles.map((t) => t.id);
+ expect(visibleToggleIds).toEqual(expect.arrayContaining(expectedToggleIds));
+ });
+
+ test("should return an empty array for an unknown solution type", () => {
+ const solutionType = "UNKNOWN_TYPE";
+ const visibleToggles = getVisibleToggles(solutionType);
+ expect(visibleToggles).toEqual([]);
+ expect(visibleToggles).toHaveLength(0);
+ });
+
+ test("should return an empty array for null or undefined solution type", () => {
+ expect(getVisibleToggles(null)).toEqual([]);
+ expect(getVisibleToggles(undefined)).toEqual([]);
+ });
+});
\ No newline at end of file
diff --git a/src/TripObjects.js b/src/TripObjects.js
index e7f67ae..86348d7 100644
--- a/src/TripObjects.js
+++ b/src/TripObjects.js
@@ -138,19 +138,19 @@ export class TripObjects {
path: tripCoords,
geodesic: true,
strokeColor: strokeColor,
- strokeOpacity: 0.3,
- strokeWeight: 6,
+ strokeOpacity: 0.6,
+ strokeWeight: 5,
clickable: true,
zIndex: 1,
map: this.map,
});
google.maps.event.addListener(path, "mouseover", () => {
- path.setOptions({ strokeOpacity: 0.7, strokeWeight: 8, zIndex: 100 });
+ path.setOptions({ strokeOpacity: 0.8, strokeWeight: 6, zIndex: 100 });
});
google.maps.event.addListener(path, "mouseout", () => {
- path.setOptions({ strokeOpacity: 0.3, strokeWeight: 6, zIndex: 1 });
+ path.setOptions({ strokeOpacity: 0.6, strokeWeight: 5, zIndex: 1 });
});
// Handle click on polyline but pass the event through to the map