aboutsummaryrefslogtreecommitdiff
path: root/src/map/MapPositions.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/map/MapPositions.js')
-rw-r--r--src/map/MapPositions.js216
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;