diff options
Diffstat (limited to 'src/map/MapPositions.js')
-rw-r--r-- | src/map/MapPositions.js | 216 |
1 files changed, 216 insertions, 0 deletions
diff --git a/src/map/MapPositions.js b/src/map/MapPositions.js new file mode 100644 index 00000000..751c61b9 --- /dev/null +++ b/src/map/MapPositions.js @@ -0,0 +1,216 @@ +import { useId, useCallback, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { useMediaQuery } from '@mui/material'; +import { useTheme } from '@mui/styles'; +import { map } from './core/MapView'; +import { formatTime, getStatusColor } from '../common/util/formatter'; +import { mapIconKey } from './core/preloadImages'; +import { findFonts } from './core/mapUtil'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; + +const MapPositions = ({ positions, onClick, showStatus, selectedPosition, titleField }) => { + const id = useId(); + const clusters = `${id}-clusters`; + const selected = `${id}-selected`; + + const theme = useTheme(); + const desktop = useMediaQuery(theme.breakpoints.up('md')); + const iconScale = useAttributePreference('iconScale', desktop ? 0.75 : 1); + + const devices = useSelector((state) => state.devices.items); + const selectedDeviceId = useSelector((state) => state.devices.selectedId); + + const mapCluster = useAttributePreference('mapCluster', true); + const hours12 = usePreference('twelveHourFormat'); + const directionType = useAttributePreference('mapDirection', 'selected'); + + const createFeature = (devices, position, selectedPositionId) => { + const device = devices[position.deviceId]; + let showDirection; + switch (directionType) { + case 'none': + showDirection = false; + break; + case 'all': + showDirection = true; + break; + default: + showDirection = selectedPositionId === position.id; + break; + } + return { + id: position.id, + deviceId: position.deviceId, + name: device.name, + fixTime: formatTime(position.fixTime, 'seconds', hours12), + category: mapIconKey(device.category), + color: showStatus ? position.attributes.color || getStatusColor(device.status) : 'neutral', + rotation: position.course, + direction: showDirection, + }; + }; + + const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer'; + const onMouseLeave = () => map.getCanvas().style.cursor = ''; + + const onMapClick = useCallback((event) => { + if (!event.defaultPrevented && onClick) { + onClick(); + } + }, [onClick]); + + const onMarkerClick = useCallback((event) => { + event.preventDefault(); + const feature = event.features[0]; + if (onClick) { + onClick(feature.properties.id, feature.properties.deviceId); + } + }, [onClick]); + + const onClusterClick = useCallback((event) => { + event.preventDefault(); + const features = map.queryRenderedFeatures(event.point, { + layers: [clusters], + }); + const clusterId = features[0].properties.cluster_id; + map.getSource(id).getClusterExpansionZoom(clusterId, (error, zoom) => { + if (!error) { + map.easeTo({ + center: features[0].geometry.coordinates, + zoom, + }); + } + }); + }, [clusters]); + + useEffect(() => { + map.addSource(id, { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [], + }, + cluster: mapCluster, + clusterMaxZoom: 14, + clusterRadius: 50, + }); + map.addSource(selected, { + type: 'geojson', + data: { + type: 'FeatureCollection', + features: [], + }, + }); + [id, selected].forEach((source) => { + map.addLayer({ + id: source, + type: 'symbol', + source, + filter: ['!has', 'point_count'], + layout: { + 'icon-image': '{category}-{color}', + 'icon-size': iconScale, + 'icon-allow-overlap': true, + 'text-field': `{${titleField || 'name'}}`, + 'text-allow-overlap': true, + 'text-anchor': 'bottom', + 'text-offset': [0, -2 * iconScale], + 'text-font': findFonts(map), + 'text-size': 12, + }, + paint: { + 'text-halo-color': 'white', + 'text-halo-width': 1, + }, + }); + map.addLayer({ + id: `direction-${source}`, + type: 'symbol', + source, + filter: [ + 'all', + ['!has', 'point_count'], + ['==', 'direction', true], + ], + layout: { + 'icon-image': 'direction', + 'icon-size': iconScale, + 'icon-allow-overlap': true, + 'icon-rotate': ['get', 'rotation'], + 'icon-rotation-alignment': 'map', + }, + }); + + map.on('mouseenter', source, onMouseEnter); + map.on('mouseleave', source, onMouseLeave); + map.on('click', source, onMarkerClick); + }); + map.addLayer({ + id: clusters, + type: 'symbol', + source: id, + filter: ['has', 'point_count'], + layout: { + 'icon-image': 'background', + 'icon-size': iconScale, + 'text-field': '{point_count_abbreviated}', + 'text-font': findFonts(map), + 'text-size': 14, + }, + }); + + map.on('mouseenter', clusters, onMouseEnter); + map.on('mouseleave', clusters, onMouseLeave); + map.on('click', clusters, onClusterClick); + map.on('click', onMapClick); + + return () => { + map.off('mouseenter', clusters, onMouseEnter); + map.off('mouseleave', clusters, onMouseLeave); + map.off('click', clusters, onClusterClick); + map.off('click', onMapClick); + + if (map.getLayer(clusters)) { + map.removeLayer(clusters); + } + + [id, selected].forEach((source) => { + map.off('mouseenter', source, onMouseEnter); + map.off('mouseleave', source, onMouseLeave); + map.off('click', source, onMarkerClick); + + if (map.getLayer(source)) { + map.removeLayer(source); + } + if (map.getLayer(`direction-${source}`)) { + map.removeLayer(`direction-${source}`); + } + if (map.getSource(source)) { + map.removeSource(source); + } + }); + }; + }, [mapCluster, clusters, onMarkerClick, onClusterClick]); + + useEffect(() => { + [id, selected].forEach((source) => { + map.getSource(source)?.setData({ + type: 'FeatureCollection', + features: positions.filter((it) => devices.hasOwnProperty(it.deviceId)) + .filter((it) => (source === id ? it.deviceId !== selectedDeviceId : it.deviceId === selectedDeviceId)) + .map((position) => ({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [position.longitude, position.latitude], + }, + properties: createFeature(devices, position, selectedPosition && selectedPosition.id), + })), + }); + }); + }, [mapCluster, clusters, onMarkerClick, onClusterClick, devices, positions, selectedPosition]); + + return null; +}; + +export default MapPositions; |