diff options
author | Anton Tananaev <anton.tananaev@gmail.com> | 2020-10-24 16:42:29 -0700 |
---|---|---|
committer | Anton Tananaev <anton.tananaev@gmail.com> | 2020-10-24 16:42:29 -0700 |
commit | e9f0913d2b1b66764931b6c1235877a44e72890b (patch) | |
tree | 404241616fcd340dd5b8b16e699ac8c1e1be2bf1 | |
parent | 7806cb2d725ab1aa4f66db86bc376a027dda6df5 (diff) | |
download | trackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.tar.gz trackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.tar.bz2 trackermap-web-e9f0913d2b1b66764931b6c1235877a44e72890b.zip |
Refactor map implementation
-rw-r--r-- | modern/src/MainPage.js | 7 | ||||
-rw-r--r-- | modern/src/map/MainMap.js | 126 | ||||
-rw-r--r-- | modern/src/map/Map.js | 81 | ||||
-rw-r--r-- | modern/src/map/PositionsMap.js | 112 | ||||
-rw-r--r-- | modern/src/map/StatusView.js (renamed from modern/src/StatusView.js) | 4 | ||||
-rw-r--r-- | modern/src/map/mapManager.js | 125 |
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, -}; |