From 55ae0f20b9015f690549619419b3013ecbafceb3 Mon Sep 17 00:00:00 2001 From: Paul Adamsen <117673433+pauladamseniii@users.noreply.github.com> Date: Fri, 28 Apr 2023 16:56:41 -0400 Subject: [PATCH] CEC-3933 - use VehiclePaths for location drawing (#306) * CEC-3933 - Parse VehiclePaths location data * changes * fixes * stuff * sort of works * fix * progress * refactor * fix vehicle paths query * digital twin shows map * new dashboard * wider digital twin map * snapshot * latest; using polylines * lag lng changes * stuff * path showing up * stuff * things * revert home page * whitespace * validation * more stuff * fix button issue * tests pass without mocking data * fix code smells * remove map from digital twin, add to tab * fix bug * marker click event working * individual colors * possible fix * fix warning * merge and remove unused code * small fixes * re add dashboard * snaps --- .../App/__snapshots__/App.test.js.snap | 1569 +++++++++++------ src/components/Cars/CANSignals/index.jsx | 5 +- src/components/Cars/Status/DigitalTwinTab.jsx | 6 +- .../DigitalTwinTab.test.jsx.snap | 128 ++ src/components/Contexts/VehicleContext.jsx | 15 +- .../Contexts/__mocks__/VehicleContext.jsx | 24 +- src/components/Layouts/SideMenu.jsx | 3 + .../__snapshots__/SideMenu.test.jsx.snap | 192 +- src/components/VehicleMap/popup.jsx | 4 +- src/components/VehiclePathsMap/index.jsx | 253 +++ src/services/__mocks__/vehiclesAPI.js | 60 +- src/services/customDashboards.js | 27 + src/services/vehiclesAPI.js | 13 +- src/utils/locations.js | 9 + 14 files changed, 1684 insertions(+), 624 deletions(-) create mode 100644 src/components/VehiclePathsMap/index.jsx diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index b0da1d2..909fc68 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -269,44 +269,72 @@ exports[`App Route / authenticated 1`] = ` /> -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • - Invalid Dashboard + Vehicle Map
  • @@ -926,44 +954,72 @@ exports[`App Route /dashboards/0 authenticated 1`] = ` /> -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • - - Invalid Dashboard - +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + @@ -1418,44 +1593,72 @@ exports[`App Route /home authenticated 1`] = ` />
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • +
    +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • { useEffect(() => { setVIN(vin); + return () => { + setVIN(null) + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [vin]); @@ -45,7 +48,7 @@ const Main = ({ vin }) => { }; const CANSignals = (props) => ( - +
    ); diff --git a/src/components/Cars/Status/DigitalTwinTab.jsx b/src/components/Cars/Status/DigitalTwinTab.jsx index 40673d3..b7254d6 100644 --- a/src/components/Cars/Status/DigitalTwinTab.jsx +++ b/src/components/Cars/Status/DigitalTwinTab.jsx @@ -10,6 +10,7 @@ import { VehicleProvider } from "../../Contexts/VehicleContext"; import DigitalTwin from "../../DigitalTwin"; +import VehiclePathsMap from "../../VehiclePathsMap"; import useStyles from "../../useStyles"; const REQUEST_INTERVAL = 10000; @@ -57,7 +58,10 @@ const Main = (props) => {
    ICC Connected: {carState?.online_hmi.toString()}
    - + +
    + +
    )} diff --git a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap index d487b3c..196e03f 100644 --- a/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap +++ b/src/components/Cars/Status/__snapshots__/DigitalTwinTab.test.jsx.snap @@ -220,6 +220,134 @@ exports[`DigitalTwinTab Render 1`] = `

    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 31d0eb6..8a77948 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -88,7 +88,19 @@ export const VehicleProvider = ({ children }) => { setBusy(true); const result = await api.getLocations(token); if (result.error) - throw new Error(`Get locations error. ${result.message}`); + throw new Error(`Get locations vehicle paths error. ${result.message}`); + return result; + } finally { + setBusy(false); + } + }; + + const getLocationsVehiclePaths = async (token, vinsParam) => { + try { + setBusy(true); + const result = await api.getLocationsVehiclePaths(token, vinsParam); + if (result.error) + throw new Error(`Get locations vehicle paths error. ${result.message}`); return result; } finally { setBusy(false); @@ -263,6 +275,7 @@ export const VehicleProvider = ({ children }) => { getCANSignals, getECUs, getLocations, + getLocationsVehiclePaths, getModels, getState, getYears, diff --git a/src/components/Contexts/__mocks__/VehicleContext.jsx b/src/components/Contexts/__mocks__/VehicleContext.jsx index c545745..3c76ba1 100644 --- a/src/components/Contexts/__mocks__/VehicleContext.jsx +++ b/src/components/Contexts/__mocks__/VehicleContext.jsx @@ -98,15 +98,14 @@ export const useVehicleContext = () => ({ vehicles, years, addVehicle: jest.fn(), - getConnections: jest.fn((vins, _token) => { - const result = {}; - - vins.forEach((vin) => { - result[vin] = true; - }); - - return result; - }), + getConnections: jest + .fn().mockImplementation((vins, _token) => { + const result = {}; + vins.forEach((vin) => { + result[vin] = true; + }); + return Promise.resolve(result); + }), getECUs: jest.fn(() => { return { data: [ @@ -134,6 +133,13 @@ export const useVehicleContext = () => ({ .mockResolvedValue([ { altitude: 5, longitude: 10, latitude: 15, vin: "TESTVIN123" }, ]), + getLocationsVehiclePaths: jest + .fn() + .mockResolvedValue({ + // tests only pass without mocking the data here + // '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]], + // '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]], + }), getModels: jest.fn(() => { models = ["Ocean", "PEAR"]; }), diff --git a/src/components/Layouts/SideMenu.jsx b/src/components/Layouts/SideMenu.jsx index d17e27a..f0d24cf 100644 --- a/src/components/Layouts/SideMenu.jsx +++ b/src/components/Layouts/SideMenu.jsx @@ -12,6 +12,8 @@ import { default as React, useEffect, useState } from "react"; import { hasRole, Permissions } from "../../utils/roles"; import { useUserContext } from "../Contexts/UserContext"; import { ExpandableSideMenuItem, MenuItem } from "./MenuItem"; +import { getCustomDashboardSubmenu } from "../../services/customDashboards" + const menuData = [ { label: "Home", @@ -48,6 +50,7 @@ const menuData = [ url: `${process.env.REACT_APP_SUPERSET_URL}/login`, icon: , rolesPerProvider: Permissions.FiskerMagnaRead, + submenus: getCustomDashboardSubmenu(Permissions.FiskerMagnaRead), }, { label: "Suppliers", diff --git a/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap b/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap index 953e807..44be0aa 100644 --- a/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap +++ b/src/components/Layouts/__snapshots__/SideMenu.test.jsx.snap @@ -188,44 +188,72 @@ exports[`SideMenu Authenticated 1`] = ` />
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • -
  • - -
    +
  • + - - -
    + +
    +
    + + Datascope + +
    +
    +
  • + +
  • { {vin} {" "} - - + +
    diff --git a/src/components/VehiclePathsMap/index.jsx b/src/components/VehiclePathsMap/index.jsx new file mode 100644 index 0000000..97f7961 --- /dev/null +++ b/src/components/VehiclePathsMap/index.jsx @@ -0,0 +1,253 @@ +import { Button } from "@material-ui/core"; +import L from "leaflet"; +import React, { useEffect, useState } from "react"; +import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap } from "react-leaflet"; +import useStyles from "../useStyles"; + +import GrayMarkerIcon from "../../assets/gray-marker.png"; +import GreenMarkerIcon from "../../assets/green-marker.png"; +import { logger } from "../../services/monitoring"; +import { ValidateLocationVehiclePathsData } from "../../utils/locations"; +import { useUserContext } from "../Contexts/UserContext"; +import { useVehicleContext, VehicleProvider } from "../Contexts/VehicleContext"; +import { VehiclePopUp } from "../VehicleMap/popup"; + +const ComponentVehiclePathsMap = (props) => { + const classes = useStyles(); + const { + token: { + idToken: { jwtToken: token }, + }, + } = useUserContext(); + const { getConnections, getLocationsVehiclePaths, getState } = useVehicleContext(); + + const REQUEST_INTERVAL = 10000; + + const [center, setCenter] = useState([0, 0]); + const [zoom, setZoom] = useState(2); + const [markers, setMarkers] = useState([]); + const [connections, setConnections] = useState({}); + + useEffect(() => { + if (!token) return; + retrieveAndStoreLocations(token).then((points) => { + centerAroundMarkers(points); + }); + const id = setInterval(function () { + retrieveAndStoreLocations(token); + }, REQUEST_INTERVAL); + return () => { + clearInterval(id); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]); + + const retrieveAndStoreLocations = (accessToken) => { + let vinsToShowOnMap = [...props.vinsToShowOnMapColors.keys()]; + let vinsParam = "" + for (let vinToShowOnMap of vinsToShowOnMap) { + vinsParam += "vins=" + vinsParam += vinToShowOnMap + vinsParam += "&" + } + vinsParam += "lookback_hours=" + vinsParam += props.lookbackHours + + return getLocationsVehiclePaths(accessToken, vinsParam) + .then((result) => { + let resultArray = Object.entries(result) + const points = [] + + // validate each location + for (let vinLocations of resultArray) { + let path = [] + path[0] = vinLocations[0] + path[1] = [] + for (let location of vinLocations[1]) { + if (ValidateLocationVehiclePathsData(location) !== false) { + path[1].push(location); + } + } + points.push(path) + } + + setMarkers(points); + return points; + }) + .catch((error) => logger.warn(error.stack)); + }; + + const centerAroundMarkers = (points) => { + // default center + let center = [37.0902, -95.7129] + + // center is the very first geographical point + if (points && points[0] && points[0][1] && points[0][1][0]) { + center = points[0][1][0] + } + + setCenter(center); + setZoom(4.5); + }; + + useEffect(() => { + if (!token) return; + + const vins = [] + + for (let vinLocations of markers) { + vins.push(vinLocations[0]) + } + + if (vins.length === 0) return; + + getConnections(vins, token).then((conns) => { + setConnections(conns); + }); + // eslint-disable-next-line + }, [markers, token]); + + const [selectedVIN, setSelectedVIN] = useState(null); + const [carState, setCarState] = useState(null); + + useEffect(() => { + if (selectedVIN != null) { + retrieveAndStoreCarState(selectedVIN); + const id = setInterval(function () { + retrieveAndStoreCarState(selectedVIN); + }, REQUEST_INTERVAL); + return () => { + clearInterval(id); + }; + } + // eslint-disable-next-line + }, [selectedVIN]); + + const selectCar = (e, vin) => { + e.preventDefault(); + setSelectedVIN(vin); + }; + + const retrieveAndStoreCarState = (vin) => { + getState(token, vin).then((results) => { + setCarState({ ...results.data, vin: vin }); + }); + }; + + const handleClose = () => { + setSelectedVIN(null); + setCarState(null); + }; + + const isOnline = (vin) => { + return connections[vin]; + }; + + const getZIndex = (vin) => { + if (isOnline(vin)) return 1000; + return 0; + }; + + function getCarIcon(vin) { + let icon = GrayMarkerIcon; + + if (isOnline(vin)) { + icon = GreenMarkerIcon; + } + + return new L.Icon({ + iconUrl: icon, + iconAnchor: [24, 42], + }); + } + + return ( + + + + {markers && markers.map((vinLocations) => ( +
    + + {vinLocations[1][0] && + { + setCenter(vinLocations[1][0]); + setZoom(16); + }, + }} + > + +
    +

    + {vinLocations[0]} +

    + +
    +
    +
    + } +
    + ))} + + {carState ? ( + + ) : null} +
    + ); +}; + +const CenterFocus = ({ center, zoom }) => { + const map = useMap(); + + useEffect(() => { + if (center[0] === 0 && center[1] === 0) { + map.flyTo([0, 0], 2, { duration: 1.5 }); + } else { + map.flyTo(center, zoom, { duration: 1.5 }); + } + }, [center, zoom, map]); + + return null; +}; + +const VehiclePathsMap = (props) => ( + + + +); + +export default VehiclePathsMap; diff --git a/src/services/__mocks__/vehiclesAPI.js b/src/services/__mocks__/vehiclesAPI.js index 4b2844e..369bda6 100644 --- a/src/services/__mocks__/vehiclesAPI.js +++ b/src/services/__mocks__/vehiclesAPI.js @@ -55,13 +55,15 @@ const ecusData = [ }, ]; -const signals = {data:[ - { - timestamp: "2021-07-14T20:09:40.98187Z", - name: "signal", - value: 123 - }, -]}; +const signals = { + data: [ + { + timestamp: "2021-07-14T20:09:40.98187Z", + name: "signal", + value: 123 + }, + ], +}; const trexLogs = { RealOffset: 0, @@ -127,6 +129,12 @@ const vehiclesAPI = { .mockResolvedValue([ { altitude: 5, longitude: 10, latitude: 15, vin: "TESTVIN123" }, ]), + getLocationsVehiclePaths: async () => { + return { + '3FAFP13P31R199430': [[16.891136999999986, 26.832352999999955], [56.891136999999986, 66.832352999999955], [26.891136999999986, 36.832352999999955]], + '3FAFP13P71R199060': [[36.891136999999986, 46.832352999999955], [76.891136999999986, 16.832352999999955]], + }; + }, getVehicle: async (vin) => { const index = data.findIndex(element => element.vin === vin); return data[index]; @@ -134,7 +142,7 @@ const vehiclesAPI = { getVehicles: async () => { return { data }; }, - getFleets: async (vin) => {return { data: ["fleet1", "fleet2"]}}, + getFleets: async (vin) => { return { data: ["fleet1", "fleet2"] } }, getYears: async () => { return { data: [2021, 2022], @@ -152,29 +160,29 @@ const vehiclesAPI = { return vehicle; }, getCANSignals: async (vin, vehicle) => { - return signals; + return signals; }, getTRexLogs: async (vin, date, offset, count, direction, token) => { return trexLogs; }, getVersionLog: async (vin) => ({ - "data": [ - { - "id": 1, - "vin": "${vin}", - "version_source": "TREX", - "version": "0.9.56", - "created_at": "2023-01-13T02:11:33.327214Z" - }, - { - "id": 2, - "vin": "${vin}", - "version_source": "DBC", - "version": "386c18977a1be3cda60c953e5902c680dbe82b89523f2527e80cd9db863db991", - "created_at": "2023-01-13T02:11:33.330932Z" - } - ], - "total": 2 + "data": [ + { + "id": 1, + "vin": "${vin}", + "version_source": "TREX", + "version": "0.9.56", + "created_at": "2023-01-13T02:11:33.327214Z" + }, + { + "id": 2, + "vin": "${vin}", + "version_source": "DBC", + "version": "386c18977a1be3cda60c953e5902c680dbe82b89523f2527e80cd9db863db991", + "created_at": "2023-01-13T02:11:33.330932Z" + } + ], + "total": 2 }) }; diff --git a/src/services/customDashboards.js b/src/services/customDashboards.js index 8f9fe65..80dae84 100644 --- a/src/services/customDashboards.js +++ b/src/services/customDashboards.js @@ -1,8 +1,30 @@ +import VehiclePathsMap from "../components/VehiclePathsMap"; + const INVALID_DASHBOARD = { label: "Invalid Dashboard", error: "Invalid Dashboard" } +const vinsToShowOnMapColors = new Map([ + ['3FAFP13P71R199267', 'red'], + ['3FAFP13P71R199270', 'orange'], + ['3FAFP13P71R199222', 'blue'], + ['3FAFP13P61R199339', 'yellow'], + ['3FAFP13P71R199057', 'turquoise'], + ['3FAFP13P61R199387', 'lime'], + ['3FAFP13P71R199334', 'purple'], + ['3FAFP13P71R199284', 'green'], + ['3FAFP13P71R199303', 'sienna'], + ['3FAFP13P31R199430', 'navy'], + ['3FAFP13P81R199083', 'cadetblue'], + ['3FAFP13P71R199060', 'coral'], + ['3FAFP13P71R199317', 'darkkhaki'], + ['3FAFP13P71R199320', 'fuchsia'], + ['3FAFP13P61R199390', 'indigo'], + ['3FAFP13P61R199373', 'cyan'], +]) +const lookbackHours = 24 + export const CustomDashboardList = [ /* { @@ -14,6 +36,11 @@ export const CustomDashboardList = [ component: } */ + + { + label: "Vehicle Map", + component: + } ]; export const getCustomDashboard = (index) => { diff --git a/src/services/vehiclesAPI.js b/src/services/vehiclesAPI.js index 80345e4..8ca9e1f 100644 --- a/src/services/vehiclesAPI.js +++ b/src/services/vehiclesAPI.js @@ -76,6 +76,17 @@ const vehiclesAPI = { .then(fetchRespHandler) .catch(errorHandler), + getLocationsVehiclePaths: async (token, vinsParam) => + fetch(`${API_ENDPOINT}/vehicle_paths?${vinsParam}`, { + method: "GET", + headers: Object.assign( + { "Content-Type": "application/json" }, + getAuthHeaderOptions(token) + ), + }) + .then(fetchRespHandler) + .catch(errorHandler), + getState: async (token, vin) => fetch(`${API_ENDPOINT}/carstate?vin=${vin}`, { method: "GET", @@ -185,7 +196,7 @@ const vehiclesAPI = { .then(fetchRespHandler) .catch(errorHandler), - getVersionLog: async ({vin, ...search}, token) => { + getVersionLog: async ({ vin, ...search }, token) => { const u = addQueryParams(`${API_ENDPOINT}/vehicle/${vin}/version/logs`, search); return fetch(u, { method: "GET", diff --git a/src/utils/locations.js b/src/utils/locations.js index f4fdaba..542418c 100644 --- a/src/utils/locations.js +++ b/src/utils/locations.js @@ -10,6 +10,15 @@ export const ValidateLocationData = (location) => { return true; } +export const ValidateLocationVehiclePathsData = (location) => { + if (location.length < 2) { + return false; + } + + // location is an array of length 2 as { float64, float64} + return !(Math.abs(location[0]) > 90 || Math.abs(location[1]) > 180); +} + export const ValidateLocationByParam = (parameter, value) => { if (invalidLocation === value) return false; switch (parameter) {