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
This commit is contained in:
Paul Adamsen
2023-04-28 16:56:41 -04:00
committed by GitHub
parent 8dfc516986
commit 55ae0f20b9
14 changed files with 1684 additions and 624 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,9 @@ const Main = ({ vin }) => {
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) => (
<CANSignalProvider {...{token:props.token}}>
<CANSignalProvider {...{ token: props.token }}>
<Main {...props} />
</CANSignalProvider>
);

View File

@@ -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) => {
<div>
<b>ICC Connected</b>: {carState?.online_hmi.toString()}
</div>
<DigitalTwin {...carState} />
<DigitalTwin {...carState} vin={vin} />
<div style={{ width: '100vh' }}>
<VehiclePathsMap vinsToShowOnMapColors={new Map([[vin, 'navy']])} lookbackHours={24} />
</div>
</>
)}
</div>

View File

@@ -220,6 +220,134 @@ exports[`DigitalTwinTab Render 1`] = `
</p>
</div>
</div>
<div
style="width: 100vh;"
>
<div
data-testid="mocked-vehicleprovider"
>
<div
class="leaflet-container leaflet-touch leaflet-grab leaflet-touch-drag leaflet-touch-zoom"
style="width: 100%; height: 900px; position: relative;"
tabindex="0"
>
<div
class="leaflet-pane leaflet-map-pane"
style="left: 0px; top: 0px;"
>
<div
class="leaflet-pane leaflet-tile-pane"
>
<div
class="leaflet-layer "
style="z-index: 1;"
>
<div
class="leaflet-tile-container leaflet-zoom-animated"
style="z-index: 18; left: 0px; top: 0px;"
>
<img
alt=""
class="leaflet-tile"
role="presentation"
src="https://b.tile.openstreetmap.org/5/7/12.png"
style="width: 256px; height: 256px; left: -126px; top: -114px;"
/>
</div>
</div>
</div>
<div
class="leaflet-pane leaflet-overlay-pane"
/>
<div
class="leaflet-pane leaflet-shadow-pane"
/>
<div
class="leaflet-pane leaflet-marker-pane"
/>
<div
class="leaflet-pane leaflet-tooltip-pane"
/>
<div
class="leaflet-pane leaflet-popup-pane"
/>
</div>
<div
class="leaflet-control-container"
>
<div
class="leaflet-top leaflet-left"
>
<div
class="leaflet-control-zoom leaflet-bar leaflet-control"
>
<a
aria-disabled="false"
aria-label="Zoom in"
class="leaflet-control-zoom-in"
href="#"
role="button"
title="Zoom in"
>
<span
aria-hidden="true"
>
+
</span>
</a>
<a
aria-disabled="false"
aria-label="Zoom out"
class="leaflet-control-zoom-out"
href="#"
role="button"
title="Zoom out"
>
<span
aria-hidden="true"
>
</span>
</a>
</div>
</div>
<div
class="leaflet-top leaflet-right"
/>
<div
class="leaflet-bottom leaflet-left"
/>
<div
class="leaflet-bottom leaflet-right"
>
<div
class="leaflet-control-attribution leaflet-control"
>
<a
href="https://leafletjs.com"
title="A JavaScript library for interactive maps"
>
Leaflet
</a>
<span
aria-hidden="true"
>
|
</span>
©
<a
href="http://osm.org/copyright"
>
OpenStreetMap
</a>
contributors
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -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,

View File

@@ -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"];
}),

View File

@@ -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: <AssessmentIcon />,
rolesPerProvider: Permissions.FiskerMagnaRead,
submenus: getCustomDashboardSubmenu(Permissions.FiskerMagnaRead),
},
{
label: "Suppliers",

View File

@@ -188,44 +188,72 @@ exports[`SideMenu Authenticated 1`] = `
/>
</a>
</li>
<li>
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="https://dev-superset.cloud.fiskerinc.com/login"
rel="noopener"
role="button"
tabindex="0"
target="_blank"
>
<div
class="MuiListItemIcon-root"
<span>
<li>
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="https://dev-superset.cloud.fiskerinc.com/login"
rel="noopener"
role="button"
tabindex="0"
target="_blank"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
<div
class="MuiListItemIcon-root"
>
<path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Datascope
</span>
</div>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
class="MuiTouchRipple-root"
/>
</a>
</li>
</span>
<ul
style="margin-left: 50px;"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/dashboards/0"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
Datascope
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Vehicle Map
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul>
<li>
<a
aria-disabled="false"
@@ -469,44 +497,72 @@ exports[`SideMenu Magna Authenticated 1`] = `
/>
</a>
</li>
<li>
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="https://dev-superset.cloud.fiskerinc.com/login"
rel="noopener"
role="button"
tabindex="0"
target="_blank"
>
<div
class="MuiListItemIcon-root"
<span>
<li>
<a
aria-disabled="false"
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiButtonBase-root MuiListItem-root makeStyles-menuExternalLink-0 MuiListItem-gutters MuiListItem-button MuiTypography-colorPrimary"
href="https://dev-superset.cloud.fiskerinc.com/login"
rel="noopener"
role="button"
tabindex="0"
target="_blank"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
<div
class="MuiListItemIcon-root"
>
<path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"
/>
</svg>
</div>
<div
class="MuiListItemText-root"
>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Datascope
</span>
</div>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
class="MuiTouchRipple-root"
/>
</a>
</li>
</span>
<ul
style="margin-left: 50px;"
>
<li>
<a
aria-disabled="false"
class="MuiButtonBase-root MuiListItem-root MuiListItem-gutters MuiListItem-button"
href="/dashboards/0"
role="button"
tabindex="0"
>
<div
class="MuiListItemText-root"
>
Datascope
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
<span
class="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
>
Vehicle Map
</span>
</div>
<span
class="MuiTouchRipple-root"
/>
</a>
</li>
</ul>
<span>
<li>
<a

View File

@@ -41,8 +41,8 @@ const VehiclePopUp = (props) => {
<DialogTitle align="center" onClose={onClose}>
{vin}
{" "}
<IconButton>
<VisibilityIcon fontSize="inherit" onClick={toggleView} />
<IconButton onClick={toggleView}>
<VisibilityIcon fontSize="inherit" />
</IconButton>
</DialogTitle>
<div align="center" className={classes.popUpContent}>

View File

@@ -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 (
<MapContainer
center={center}
zoom={zoom}
style={{
width: "100%",
height: "900px",
}}
>
<TileLayer
attribution='&copy <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<CenterFocus center={center} zoom={zoom} />
{markers && markers.map((vinLocations) => (
<div key={vinLocations[0]}>
<Polyline
key={'line' + vinLocations[0]}
positions={vinLocations[1]}
pathOptions={{
color: props.vinsToShowOnMapColors && props.vinsToShowOnMapColors.get(vinLocations[0]),
}}
/>
{vinLocations[1][0] &&
<Marker
icon={getCarIcon(vinLocations[0])}
key={'marker' + vinLocations[0]}
position={vinLocations[1][0]}
title={vinLocations[0]}
opacity={0.9}
zIndexOffset={getZIndex(vinLocations[0])}
eventHandlers={{
click: () => {
setCenter(vinLocations[1][0]);
setZoom(16);
},
}}
>
<Popup>
<div align="center">
<p className={classes.markerTitle}>
<b>{vinLocations[0]}</b>
</p>
<Button
type="submit"
variant="contained"
color="primary"
onClick={(e) => selectCar(e, vinLocations[0])}
>
View Stats
</Button>
</div>
</Popup>
</Marker>
}
</div>
))}
{carState ? (
<VehiclePopUp
{...carState}
className={classes.popup}
onClose={handleClose}
/>
) : null}
</MapContainer>
);
};
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) => (
<VehicleProvider>
<ComponentVehiclePathsMap vinsToShowOnMapColors={props.vinsToShowOnMapColors} lookbackHours={props.lookbackHours} />
</VehicleProvider>
);
export default VehiclePathsMap;