aboutsummaryrefslogtreecommitdiff
path: root/modern
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2021-06-26 18:25:45 -0700
committerAnton Tananaev <anton.tananaev@gmail.com>2021-06-26 18:25:45 -0700
commitde199fa668b61ca1def0b8d18de8d666d8148361 (patch)
tree7eaace63676732087f2348c1201d580ba120b207 /modern
parent698e27607bddd10a2f5b10a5ff07692a3d1f4ae0 (diff)
downloadetbsa-traccar-web-de199fa668b61ca1def0b8d18de8d666d8148361.tar.gz
etbsa-traccar-web-de199fa668b61ca1def0b8d18de8d666d8148361.tar.bz2
etbsa-traccar-web-de199fa668b61ca1def0b8d18de8d666d8148361.zip
Implement map clustering
Diffstat (limited to 'modern')
-rw-r--r--modern/src/map/Map.js3
-rw-r--r--modern/src/map/PositionsMap.js48
-rw-r--r--modern/src/map/mapUtil.js14
3 files changed, 53 insertions, 12 deletions
diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js
index 8a43e97..014268c 100644
--- a/modern/src/map/Map.js
+++ b/modern/src/map/Map.js
@@ -37,9 +37,10 @@ const updateReadyValue = value => {
const initMap = async () => {
const background = await loadImage('images/background.svg');
+ map.addImage('background', await loadIcon(background), { pixelRatio: window.devicePixelRatio });
await Promise.all(deviceCategories.map(async category => {
if (!map.hasImage(category)) {
- const imageData = await loadIcon(category, background, `images/icon/${category}.svg`);
+ const imageData = await loadIcon(background, `images/icon/${category}.svg`);
map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio });
}
}));
diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js
index fa7b431..4a590b7 100644
--- a/modern/src/map/PositionsMap.js
+++ b/modern/src/map/PositionsMap.js
@@ -10,6 +10,7 @@ import StatusView from './StatusView';
const PositionsMap = ({ positions }) => {
const id = 'positions';
+ const clusters = `${id}-clusters`;
const history = useHistory();
const devices = useSelector(state => state.devices.items);
@@ -26,7 +27,7 @@ const PositionsMap = ({ positions }) => {
const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer';
const onMouseLeave = () => map.getCanvas().style.cursor = '';
- const onClickCallback = useCallback(event => {
+ const onMarkerClick = useCallback(event => {
const feature = event.features[0];
let coordinates = feature.geometry.coordinates.slice();
while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
@@ -50,18 +51,35 @@ const PositionsMap = ({ positions }) => {
.addTo(map);
}, [history]);
+ const onClusterClick = event => {
+ 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: zoom,
+ });
+ });
+ };
+
useEffect(() => {
map.addSource(id, {
'type': 'geojson',
'data': {
type: 'FeatureCollection',
features: [],
- }
+ },
+ 'cluster': true,
+ 'clusterMaxZoom': 14,
+ 'clusterRadius': 50,
});
map.addLayer({
'id': id,
'type': 'symbol',
'source': id,
+ 'filter': ['!', ['has', 'point_count']],
'layout': {
'icon-image': '{category}',
'icon-allow-overlap': true,
@@ -77,22 +95,42 @@ const PositionsMap = ({ positions }) => {
'text-halo-width': 1,
},
});
+ map.addLayer({
+ 'id': clusters,
+ 'type': 'symbol',
+ 'source': id,
+ 'filter': ['has', 'point_count'],
+ 'layout': {
+ 'icon-image': 'background',
+ 'text-field': '{point_count_abbreviated}',
+ 'text-font': ['Roboto Regular'],
+ 'text-size': 14,
+ },
+ });
+
map.on('mouseenter', id, onMouseEnter);
map.on('mouseleave', id, onMouseLeave);
- map.on('click', id, onClickCallback);
+ map.on('mouseenter', clusters, onMouseEnter);
+ map.on('mouseleave', clusters, onMouseLeave);
+ map.on('click', id, onMarkerClick);
+ map.on('click', clusters, onClusterClick);
return () => {
Array.from(map.getContainer().getElementsByClassName('mapboxgl-popup')).forEach(el => el.remove());
map.off('mouseenter', id, onMouseEnter);
map.off('mouseleave', id, onMouseLeave);
- map.off('click', id, onClickCallback);
+ map.off('mouseenter', clusters, onMouseEnter);
+ map.off('mouseleave', clusters, onMouseLeave);
+ map.off('click', id, onMarkerClick);
+ map.off('click', clusters, onClusterClick);
map.removeLayer(id);
+ map.removeLayer(clusters);
map.removeSource(id);
};
- }, [onClickCallback]);
+ }, [onMarkerClick]);
useEffect(() => {
map.getSource(id).setData({
diff --git a/modern/src/map/mapUtil.js b/modern/src/map/mapUtil.js
index e7dc032..4780940 100644
--- a/modern/src/map/mapUtil.js
+++ b/modern/src/map/mapUtil.js
@@ -9,8 +9,7 @@ export const loadImage = (url) => {
});
};
-export const loadIcon = async (key, background, url) => {
- const image = await loadImage(url);
+export const loadIcon = async (background, url) => {
const pixelRatio = window.devicePixelRatio;
const canvas = document.createElement('canvas');
@@ -22,10 +21,13 @@ export const loadIcon = async (key, background, url) => {
const context = canvas.getContext('2d');
context.drawImage(background, 0, 0, canvas.width, canvas.height);
- const iconRatio = 0.5;
- const imageWidth = canvas.width * iconRatio;
- const imageHeight = canvas.height * iconRatio;
- context.drawImage(image, (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
+ if (url) {
+ const image = await loadImage(url);
+ const iconRatio = 0.5;
+ const imageWidth = canvas.width * iconRatio;
+ const imageHeight = canvas.height * iconRatio;
+ context.drawImage(image, (canvas.width - imageWidth) / 2, (canvas.height - imageHeight) / 2, imageWidth, imageHeight);
+ }
return context.getImageData(0, 0, canvas.width, canvas.height);
};