aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Tananaev <anton.tananaev@gmail.com>2020-10-24 16:42:29 -0700
committerAnton Tananaev <anton.tananaev@gmail.com>2020-10-24 16:42:29 -0700
commite9f0913d2b1b66764931b6c1235877a44e72890b (patch)
tree404241616fcd340dd5b8b16e699ac8c1e1be2bf1
parent7806cb2d725ab1aa4f66db86bc376a027dda6df5 (diff)
downloadtrackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.tar.gz
trackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.tar.bz2
trackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.zip
Refactor map implementation
-rw-r--r--modern/src/MainPage.js7
-rw-r--r--modern/src/map/MainMap.js126
-rw-r--r--modern/src/map/Map.js81
-rw-r--r--modern/src/map/PositionsMap.js112
-rw-r--r--modern/src/map/StatusView.js (renamed from modern/src/StatusView.js)4
-rw-r--r--modern/src/map/mapManager.js125
6 files changed, 200 insertions, 255 deletions
diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js
index 659c324e..9f9f744b 100644
--- a/modern/src/MainPage.js
+++ b/modern/src/MainPage.js
@@ -5,8 +5,9 @@ import Drawer from '@material-ui/core/Drawer';
import ContainerDimensions from 'react-container-dimensions';
import LinearProgress from '@material-ui/core/LinearProgress';
import DevicesList from './DevicesList';
-import MainMap from './map/MainMap';
import MainToolbar from './MainToolbar';
+import Map from './map/Map';
+import PositionsMap from './map/PositionsMap';
const useStyles = makeStyles(theme => ({
root: {
@@ -53,7 +54,9 @@ const MainPage = ({ width }) => {
</Drawer>
<div className={classes.mapContainer}>
<ContainerDimensions>
- <MainMap />
+ <Map>
+ <PositionsMap />
+ </Map>
</ContainerDimensions>
</div>
</div>
diff --git a/modern/src/map/MainMap.js b/modern/src/map/MainMap.js
deleted file mode 100644
index 4ee6d4dc..00000000
--- a/modern/src/map/MainMap.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import React, { useRef, useLayoutEffect, useEffect, useState } from 'react';
-import ReactDOM from 'react-dom';
-import { Provider, useSelector } from 'react-redux';
-import mapboxgl from 'mapbox-gl';
-
-import mapManager from './mapManager';
-import store from '../store';
-import StatusView from '../StatusView';
-import { calculateBounds } from './mapUtil';
-import { useHistory } from 'react-router-dom';
-
-const MainMap = () => {
- const history = useHistory();
-
- const containerEl = useRef(null);
-
- const [mapReady, setMapReady] = useState(false);
-
- const mapCenter = useSelector(state => {
- if (state.devices.selectedId) {
- const position = state.positions.items[state.devices.selectedId] || null;
- if (position) {
- return [position.longitude, position.latitude];
- }
- }
- return null;
- });
-
- const createFeature = (state, position) => {
- const device = state.devices.items[position.deviceId] || null;
- return {
- deviceId: position.deviceId,
- name: device ? device.name : '',
- category: device && device.category || 'default',
- }
- };
-
- const positions = useSelector(state => ({
- type: 'FeatureCollection',
- features: Object.values(state.positions.items).map(position => ({
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [position.longitude, position.latitude]
- },
- properties: createFeature(state, position),
- })),
- }));
-
- useLayoutEffect(() => {
- const currentEl = containerEl.current;
- currentEl.appendChild(mapManager.element);
- if (mapManager.map) {
- mapManager.map.resize();
- }
- return () => {
- currentEl.removeChild(mapManager.element);
- };
- }, [containerEl]);
-
- useEffect(() => {
- mapManager.onMapReady(() => setMapReady(true));
- }, []);
-
- const markerClickHandler = (event) => {
- const feature = event.features[0];
- let coordinates = feature.geometry.coordinates.slice();
- while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
- coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
- }
-
- const placeholder = document.createElement('div');
- ReactDOM.render(
- <Provider store={store}>
- <StatusView deviceId={feature.properties.deviceId} onShowDetails={positionId => history.push(`/position/${positionId}`)} />
- </Provider>,
- placeholder);
-
- new mapboxgl.Popup({
- offset: 25,
- anchor: 'top'
- })
- .setDOMContent(placeholder)
- .setLngLat(coordinates)
- .addTo(mapManager.map);
- };
-
- useEffect(() => {
- if (mapReady) {
- mapManager.map.addSource('positions', {
- 'type': 'geojson',
- 'data': positions,
- });
- mapManager.addLayer('device-icon', 'positions', '{category}', '{name}', markerClickHandler);
-
- const bounds = calculateBounds(positions.features);
- if (bounds) {
- mapManager.map.fitBounds(bounds, {
- padding: 100,
- maxZoom: 9
- });
- }
-
- return () => {
- mapManager.removeLayer('device-icon', 'positions');
- };
- }
- }, [mapReady]);
-
- useEffect(() => {
- mapManager.map.easeTo({
- center: mapCenter
- });
- }, [mapCenter]);
-
- useEffect(() => {
- const source = mapManager.map.getSource('positions');
- if (source) {
- source.setData(positions);
- }
- }, [positions]);
-
- return <div style={{ width: '100%', height: '100%' }} ref={containerEl} />;
-}
-
-export default MainMap;
diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js
new file mode 100644
index 00000000..fec8d501
--- /dev/null
+++ b/modern/src/map/Map.js
@@ -0,0 +1,81 @@
+import 'mapbox-gl/dist/mapbox-gl.css';
+import mapboxgl from 'mapbox-gl';
+import React, { useRef, useLayoutEffect, useEffect, useState } from 'react';
+import { deviceCategories } from '../common/deviceCategories';
+import { loadIcon, loadImage } from './mapUtil';
+
+const element = document.createElement('div');
+element.style.width = '100%';
+element.style.height = '100%';
+
+export const map = new mapboxgl.Map({
+ container: element,
+ style: {
+ version: 8,
+ sources: {
+ osm: {
+ type: 'raster',
+ tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
+ tileSize: 256,
+ attribution: '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
+ },
+ },
+ glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf',
+ layers: [{
+ id: 'osm',
+ type: 'raster',
+ source: 'osm',
+ }],
+ },
+});
+
+map.addControl(new mapboxgl.NavigationControl());
+
+let readyListeners = [];
+
+const onMapReady = listener => {
+ if (!readyListeners) {
+ listener();
+ } else {
+ readyListeners.push(listener);
+ }
+};
+
+map.on('load', async () => {
+ const background = await loadImage('images/background.svg');
+ await Promise.all(deviceCategories.map(async category => {
+ const imageData = await loadIcon(category, background, `images/icon/${category}.svg`);
+ map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio });
+ }));
+ if (readyListeners) {
+ readyListeners.forEach(listener => listener());
+ readyListeners = null;
+ }
+});
+
+const Map = ({ children }) => {
+ const containerEl = useRef(null);
+
+ const [mapReady, setMapReady] = useState(false);
+
+ useEffect(() => onMapReady(() => setMapReady(true)), []);
+
+ useLayoutEffect(() => {
+ const currentEl = containerEl.current;
+ currentEl.appendChild(element);
+ if (map) {
+ map.resize();
+ }
+ return () => {
+ currentEl.removeChild(element);
+ };
+ }, [containerEl]);
+
+ return (
+ <div style={{ width: '100%', height: '100%' }} ref={containerEl}>
+ {mapReady && children}
+ </div>
+ );
+};
+
+export default Map;
diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js
new file mode 100644
index 00000000..6a7a68bb
--- /dev/null
+++ b/modern/src/map/PositionsMap.js
@@ -0,0 +1,112 @@
+import React, { useEffect } from 'react';
+import ReactDOM from 'react-dom';
+import mapboxgl from 'mapbox-gl';
+import { Provider, useSelector } from 'react-redux';
+
+import { map } from './Map';
+import store from '../store';
+import { useHistory } from 'react-router-dom';
+import StatusView from './StatusView';
+
+const PositionsMap = () => {
+ const id = 'positions';
+
+ const history = useHistory();
+
+ const createFeature = (state, position) => {
+ const device = state.devices.items[position.deviceId] || null;
+ return {
+ deviceId: position.deviceId,
+ name: device ? device.name : '',
+ category: device && device.category || 'default',
+ }
+ };
+
+ const positions = useSelector(state => ({
+ type: 'FeatureCollection',
+ features: Object.values(state.positions.items).map(position => ({
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [position.longitude, position.latitude]
+ },
+ properties: createFeature(state, position),
+ })),
+ }));
+
+ const onMouseEnter = () => map.getCanvas().style.cursor = 'pointer';
+ const onMouseLeave = () => map.getCanvas().style.cursor = '';
+
+ const onClick = event => {
+ const feature = event.features[0];
+ let coordinates = feature.geometry.coordinates.slice();
+ while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) {
+ coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360;
+ }
+
+ const placeholder = document.createElement('div');
+ ReactDOM.render(
+ <Provider store={store}>
+ <StatusView deviceId={feature.properties.deviceId} onShowDetails={positionId => history.push(`/position/${positionId}`)} />
+ </Provider>,
+ placeholder
+ );
+
+ new mapboxgl.Popup({
+ offset: 25,
+ anchor: 'top'
+ })
+ .setDOMContent(placeholder)
+ .setLngLat(coordinates)
+ .addTo(map);
+ };
+
+ useEffect(() => {
+ map.addSource(id, {
+ 'type': 'geojson',
+ 'data': positions,
+ });
+ map.addLayer({
+ 'id': id,
+ 'type': 'symbol',
+ 'source': id,
+ 'layout': {
+ 'icon-image': '{category}',
+ 'icon-allow-overlap': true,
+ 'text-field': '{name}',
+ 'text-allow-overlap': true,
+ 'text-anchor': 'bottom',
+ 'text-offset': [0, -2],
+ 'text-font': ['Roboto Regular'],
+ 'text-size': 12,
+ },
+ 'paint': {
+ 'text-halo-color': 'white',
+ 'text-halo-width': 1,
+ },
+ });
+
+ map.on('mouseenter', id, onMouseEnter);
+ map.on('mouseleave', id, onMouseLeave);
+ map.on('click', id, onClick);
+
+ 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, onClick);
+
+ map.removeLayer(id);
+ map.removeSource(id);
+ };
+ }, []);
+
+ useEffect(() => {
+ map.getSource(id).setData(positions);
+ }, [positions]);
+
+ return null;
+}
+
+export default PositionsMap;
diff --git a/modern/src/StatusView.js b/modern/src/map/StatusView.js
index 0713d47e..3a304263 100644
--- a/modern/src/StatusView.js
+++ b/modern/src/map/StatusView.js
@@ -1,7 +1,7 @@
-import t from './common/localization'
+import t from '../common/localization'
import React from 'react';
import { useSelector } from 'react-redux';
-import { formatPosition } from './common/formatter';
+import { formatPosition } from '../common/formatter';
const StatusView = ({ deviceId, onShowDetails }) => {
const device = useSelector(state => state.devices.items[deviceId]);
diff --git a/modern/src/map/mapManager.js b/modern/src/map/mapManager.js
deleted file mode 100644
index 4ea76729..00000000
--- a/modern/src/map/mapManager.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import 'mapbox-gl/dist/mapbox-gl.css';
-import mapboxgl from 'mapbox-gl';
-import { deviceCategories } from '../common/deviceCategories';
-import { loadIcon, loadImage } from './mapUtil';
-
-let readyListeners = [];
-
-const onMapReady = listener => {
- if (!readyListeners) {
- listener();
- } else {
- readyListeners.push(listener);
- }
-};
-
-const layerClickCallbacks = {};
-const layerMouseEnterCallbacks = {};
-const layerMauseLeaveCallbacks = {};
-
-const addLayer = (id, source, icon, text, onClick) => {
- const layer = {
- 'id': id,
- 'type': 'symbol',
- 'source': source,
- 'layout': {
- 'icon-image': icon,
- 'icon-allow-overlap': true,
- },
- };
- if (text) {
- layer.layout = {
- ...layer.layout,
- 'text-field': text,
- 'text-allow-overlap': true,
- 'text-anchor': 'bottom',
- 'text-offset': [0, -2],
- 'text-font': ['Roboto Regular'],
- 'text-size': 12,
- }
- layer.paint = {
- 'text-halo-color': 'white',
- 'text-halo-width': 1,
- }
- }
- map.addLayer(layer);
-
- layerClickCallbacks[id] = onClick;
- map.on('click', id, onClick);
-
- layerMouseEnterCallbacks[id] = () => {
- map.getCanvas().style.cursor = 'pointer';
- };
- map.on('mouseenter', id, layerMouseEnterCallbacks[id]);
-
- layerMauseLeaveCallbacks[id] = () => {
- map.getCanvas().style.cursor = '';
- };
- map.on('mouseleave', id, layerMauseLeaveCallbacks[id]);
-}
-
-const removeLayer = (id, source) => {
- const popups = element.getElementsByClassName('mapboxgl-popup');
- if (popups.length) {
- popups[0].remove();
- }
-
- map.off('click', id, layerClickCallbacks[id]);
- delete layerClickCallbacks[id];
-
- map.off('mouseenter', id, layerMouseEnterCallbacks[id]);
- delete layerMouseEnterCallbacks[id];
-
- map.off('mouseleave', id, layerMauseLeaveCallbacks[id]);
- delete layerMauseLeaveCallbacks[id];
-
- map.removeLayer(id);
- map.removeSource(source);
-}
-
-const element = document.createElement('div');
-element.style.width = '100%';
-element.style.height = '100%';
-
-const map = new mapboxgl.Map({
- container: element,
- style: {
- version: 8,
- sources: {
- osm: {
- type: 'raster',
- tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
- tileSize: 256,
- attribution: '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
- },
- },
- glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf',
- layers: [{
- id: 'osm',
- type: 'raster',
- source: 'osm',
- }],
- },
-});
-
-map.addControl(new mapboxgl.NavigationControl());
-
-map.on('load', async () => {
- const background = await loadImage('images/background.svg');
- await Promise.all(deviceCategories.map(async category => {
- const imageData = await loadIcon(category, background, `images/icon/${category}.svg`);
- map.addImage(category, imageData, { pixelRatio: window.devicePixelRatio });
- }));
- if (readyListeners) {
- readyListeners.forEach(listener => listener());
- readyListeners = null;
- }
-});
-
-export default {
- element,
- map,
- onMapReady,
- addLayer,
- removeLayer,
-};