diff --git a/src/common/util/colors.js b/src/common/util/colors.js new file mode 100644 index 0000000000..1cb35c04fe --- /dev/null +++ b/src/common/util/colors.js @@ -0,0 +1,23 @@ +import { decomposeColor } from '@mui/material'; + +export const interpolateColor = (color1, color2, factor) => { + if (factor > 1) factor = 1; + if (factor < 0) factor = 0; + + const c1 = decomposeColor(color1).values; + const c2 = decomposeColor(color2).values; + + const r = Math.round(c1[0] + factor * (c2[0] - c1[0])); + const g = Math.round(c1[1] + factor * (c2[1] - c1[1])); + const b = Math.round(c1[2] + factor * (c2[2] - c1[2])); + + return `rgb(${r}, ${g}, ${b})`; +}; + +export const getSpeedColor = (color1, color2, color3, speed, max) => { + const factor = speed / max; + if (factor <= 0.5) { + return interpolateColor(color1, color2, factor * 2); + } + return interpolateColor(color2, color3, (factor - 0.5) * 2); +}; diff --git a/src/map/MapRouteCoordinates.js b/src/map/MapRouteCoordinates.js new file mode 100644 index 0000000000..9a43ce4a84 --- /dev/null +++ b/src/map/MapRouteCoordinates.js @@ -0,0 +1,90 @@ +import { useTheme } from '@mui/styles'; +import { useId, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { map } from './core/MapView'; + +const MapRouteCoordinates = ({ name, coordinates, deviceId }) => { + const id = useId(); + + const theme = useTheme(); + + const reportColor = useSelector((state) => { + const attributes = state.devices.items[deviceId]?.attributes; + if (attributes) { + const color = attributes['web.reportColor']; + if (color) { + return color; + } + } + return theme.palette.geometry.main; + }); + + useEffect(() => { + map.addSource(id, { + type: 'geojson', + data: { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [], + }, + }, + }); + map.addLayer({ + source: id, + id: `${id}-line`, + type: 'line', + layout: { + 'line-join': 'round', + 'line-cap': 'round', + }, + paint: { + 'line-color': ['get', 'color'], + 'line-width': 2, + }, + }); + map.addLayer({ + source: id, + id: `${id}-title`, + type: 'symbol', + layout: { + 'text-field': '{name}', + 'text-size': 12, + }, + paint: { + 'text-halo-color': 'white', + 'text-halo-width': 1, + }, + }); + + return () => { + if (map.getLayer(`${id}-title`)) { + map.removeLayer(`${id}-title`); + } + if (map.getLayer(`${id}-line`)) { + map.removeLayer(`${id}-line`); + } + if (map.getSource(id)) { + map.removeSource(id); + } + }; + }, []); + + useEffect(() => { + map.getSource(id)?.setData({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates, + }, + properties: { + name, + color: reportColor, + }, + }); + }, [theme, coordinates, reportColor]); + + return null; +}; + +export default MapRouteCoordinates; diff --git a/src/map/MapRoutePath.js b/src/map/MapRoutePath.js index 202691401b..716a4bd122 100644 --- a/src/map/MapRoutePath.js +++ b/src/map/MapRoutePath.js @@ -2,8 +2,9 @@ import { useTheme } from '@mui/styles'; import { useId, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { map } from './core/MapView'; +import { getSpeedColor } from '../common/util/colors'; -const MapRoutePath = ({ name, positions, coordinates }) => { +const MapRoutePath = ({ positions }) => { const id = useId(); const theme = useTheme(); @@ -19,7 +20,7 @@ const MapRoutePath = ({ name, positions, coordinates }) => { } } } - return theme.palette.geometry.main; + return null; }); useEffect(() => { @@ -46,21 +47,6 @@ const MapRoutePath = ({ name, positions, coordinates }) => { 'line-width': 2, }, }); - if (name) { - map.addLayer({ - source: id, - id: `${id}-title`, - type: 'symbol', - layout: { - 'text-field': '{name}', - 'text-size': 12, - }, - paint: { - 'text-halo-color': 'white', - 'text-halo-width': 1, - }, - }); - } return () => { if (map.getLayer(`${id}-title`)) { @@ -76,21 +62,31 @@ const MapRoutePath = ({ name, positions, coordinates }) => { }, []); useEffect(() => { - if (!coordinates) { - coordinates = positions.map((item) => [item.longitude, item.latitude]); + const maxSpeed = positions.map((item) => item.speed).reduce((a, b) => Math.max(a, b), -Infinity); + const features = []; + for (let i = 0; i < positions.length - 1; i += 1) { + features.push({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [[positions[i].longitude, positions[i].latitude], [positions[i + 1].longitude, positions[i + 1].latitude]], + }, + properties: { + color: reportColor || getSpeedColor( + theme.palette.success.main, + theme.palette.warning.main, + theme.palette.error.main, + positions[i + 1].speed, + maxSpeed, + ), + }, + }); } map.getSource(id)?.setData({ - type: 'Feature', - geometry: { - type: 'LineString', - coordinates, - }, - properties: { - name, - color: reportColor, - }, + type: 'FeatureCollection', + features, }); - }, [theme, positions, coordinates, reportColor]); + }, [theme, positions, reportColor]); return null; }; diff --git a/src/map/MapRoutePoints.js b/src/map/MapRoutePoints.js index e329da8118..929a0d5069 100644 --- a/src/map/MapRoutePoints.js +++ b/src/map/MapRoutePoints.js @@ -1,8 +1,11 @@ import { useId, useCallback, useEffect } from 'react'; +import { useTheme } from '@mui/styles'; import { map } from './core/MapView'; +import { getSpeedColor } from '../common/util/colors'; const MapRoutePoints = ({ positions, onClick }) => { const id = useId(); + const theme = useTheme(); const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer'; const onMouseLeave = () => map.getCanvas().style.cursor = ''; @@ -27,11 +30,13 @@ const MapRoutePoints = ({ positions, onClick }) => { id, type: 'symbol', source: id, + paint: { + 'text-color': ['get', 'color'], + }, layout: { - 'icon-image': 'arrow', - 'icon-allow-overlap': true, - 'icon-rotate': ['get', 'rotation'], - 'icon-rotation-alignment': 'map', + 'text-field': '▲', + 'text-allow-overlap': true, + 'text-rotate': ['get', 'rotation'], }, }); @@ -54,6 +59,7 @@ const MapRoutePoints = ({ positions, onClick }) => { }, [onMarkerClick]); useEffect(() => { + const maxSpeed = positions.map((p) => p.speed).reduce((a, b) => Math.max(a, b), -Infinity); map.getSource(id)?.setData({ type: 'FeatureCollection', features: positions.map((position, index) => ({ @@ -66,6 +72,7 @@ const MapRoutePoints = ({ positions, onClick }) => { index, id: position.id, rotation: position.course, + color: getSpeedColor(theme.palette.success.main, theme.palette.warning.main, theme.palette.error.main, position.speed, maxSpeed), }, })), }); diff --git a/src/map/core/preloadImages.js b/src/map/core/preloadImages.js index a0056d4c3b..19c3c35ea6 100644 --- a/src/map/core/preloadImages.js +++ b/src/map/core/preloadImages.js @@ -2,7 +2,6 @@ import { grey } from '@mui/material/colors'; import createPalette from '@mui/material/styles/createPalette'; import { loadImage, prepareIcon } from './mapUtil'; -import arrowSvg from '../../resources/images/arrow.svg'; import directionSvg from '../../resources/images/direction.svg'; import backgroundSvg from '../../resources/images/background.svg'; import animalSvg from '../../resources/images/icon/animal.svg'; @@ -65,7 +64,6 @@ export default async () => { const background = await loadImage(backgroundSvg); mapImages.background = await prepareIcon(background); mapImages.direction = await prepareIcon(await loadImage(directionSvg)); - mapImages.arrow = await prepareIcon(await loadImage(arrowSvg)); await Promise.all(Object.keys(mapIcons).map(async (category) => { const results = []; ['info', 'success', 'error', 'neutral'].forEach((color) => { diff --git a/src/reports/CombinedReportPage.jsx b/src/reports/CombinedReportPage.jsx index f591a7a170..46dbf66b85 100644 --- a/src/reports/CombinedReportPage.jsx +++ b/src/reports/CombinedReportPage.jsx @@ -9,7 +9,6 @@ import PageLayout from '../common/components/PageLayout'; import ReportsMenu from './components/ReportsMenu'; import { useCatch } from '../reactHelper'; import MapView from '../map/core/MapView'; -import MapRoutePath from '../map/MapRoutePath'; import useReportStyles from './common/useReportStyles'; import TableShimmer from '../common/components/TableShimmer'; import MapCamera from '../map/MapCamera'; @@ -17,6 +16,7 @@ import MapGeofence from '../map/MapGeofence'; import { formatTime } from '../common/util/formatter'; import { prefixString } from '../common/util/stringUtils'; import MapMarkers from '../map/MapMarkers'; +import MapRouteCoordinates from '../map/MapRouteCoordinates'; const CombinedReportPage = () => { const classes = useReportStyles(); @@ -62,10 +62,11 @@ const CombinedReportPage = () => { {items.map((item) => ( - ))} diff --git a/src/resources/images/arrow.svg b/src/resources/images/arrow.svg deleted file mode 100644 index d0f30a2efe..0000000000 --- a/src/resources/images/arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - -