diff options
author | Ashutosh Bishnoi <mail2bishnoi@gmail.com> | 2020-11-10 13:02:20 +0530 |
---|---|---|
committer | Ashutosh Bishnoi <mail2bishnoi@gmail.com> | 2020-11-10 13:02:20 +0530 |
commit | a948981184f4f2884b7a35b67b2389ddcd1f52a8 (patch) | |
tree | 9187834b5246734da8549735f64d2e5546c60e1e /modern/src/map | |
parent | 5a116350b3e402ef123a75451737c268a9ebddff (diff) | |
parent | 1af1545dc7ba3cfdf73a58031b37bea0ecff2e54 (diff) | |
download | trackermap-web-a948981184f4f2884b7a35b67b2389ddcd1f52a8.tar.gz trackermap-web-a948981184f4f2884b7a35b67b2389ddcd1f52a8.tar.bz2 trackermap-web-a948981184f4f2884b7a35b67b2389ddcd1f52a8.zip |
Resolving conflicts with master
Diffstat (limited to 'modern/src/map')
-rw-r--r-- | modern/src/map/AccuracyMap.js | 53 | ||||
-rw-r--r-- | modern/src/map/CurrentLocationMap.js | 21 | ||||
-rw-r--r-- | modern/src/map/CurrentPositionsMap.js | 11 | ||||
-rw-r--r-- | modern/src/map/Map.js | 103 | ||||
-rw-r--r-- | modern/src/map/PositionsMap.js | 52 | ||||
-rw-r--r-- | modern/src/map/ReplayPathMap.js | 59 | ||||
-rw-r--r-- | modern/src/map/StatusView.js | 2 | ||||
-rw-r--r-- | modern/src/map/mapStyles.js | 52 | ||||
-rw-r--r-- | modern/src/map/mapUtil.js | 24 | ||||
-rw-r--r-- | modern/src/map/switcher/switcher.css | 40 | ||||
-rw-r--r-- | modern/src/map/switcher/switcher.js | 79 |
11 files changed, 411 insertions, 85 deletions
diff --git a/modern/src/map/AccuracyMap.js b/modern/src/map/AccuracyMap.js new file mode 100644 index 00000000..e81fc8fb --- /dev/null +++ b/modern/src/map/AccuracyMap.js @@ -0,0 +1,53 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import circle from '@turf/circle'; + +import { map } from './Map'; + +const AccuracyMap = () => { + const id = 'accuracy'; + + const positions = useSelector(state => ({ + type: 'FeatureCollection', + features: Object.values(state.positions.items).filter(position => position.accuracy > 0).map(position => + circle([position.longitude, position.latitude], position.accuracy * 0.001) + ), + })); + + useEffect(() => { + map.addSource(id, { + 'type': 'geojson', + 'data': { + type: 'FeatureCollection', + features: [] + } + }); + map.addLayer({ + 'source': id, + 'id': id, + 'type': 'fill', + 'filter': [ + 'all', + ['==', '$type', 'Polygon'], + ], + 'paint': { + 'fill-color':'#3bb2d0', + 'fill-outline-color':'#3bb2d0', + 'fill-opacity':0.25, + }, + }); + + return () => { + map.removeLayer(id); + map.removeSource(id); + }; + }, []); + + useEffect(() => { + map.getSource(id).setData(positions); + }, [positions]); + + return null; +} + +export default AccuracyMap; diff --git a/modern/src/map/CurrentLocationMap.js b/modern/src/map/CurrentLocationMap.js new file mode 100644 index 00000000..31e6e285 --- /dev/null +++ b/modern/src/map/CurrentLocationMap.js @@ -0,0 +1,21 @@ +import mapboxgl from 'mapbox-gl'; +import { useEffect } from 'react'; +import { map } from './Map'; + +const CurrentLocationMap = () => { + useEffect(() => { + const control = new mapboxgl.GeolocateControl({ + positionOptions: { + enableHighAccuracy: true, + timeout: 5000, + }, + trackUserLocation: true, + }); + map.addControl(control); + return () => map.removeControl(control); + }, []); + + return null; +} + +export default CurrentLocationMap; diff --git a/modern/src/map/CurrentPositionsMap.js b/modern/src/map/CurrentPositionsMap.js new file mode 100644 index 00000000..0bfe4fcd --- /dev/null +++ b/modern/src/map/CurrentPositionsMap.js @@ -0,0 +1,11 @@ +import React, { } from 'react'; +import { useSelector } from 'react-redux'; + +import PositionsMap from './PositionsMap'; + +const CurrentPositionsMap = () => { + const positions = useSelector(state => Object.values(state.positions.items)); + return (<PositionsMap positions={positions} />); +} + +export default CurrentPositionsMap; diff --git a/modern/src/map/Map.js b/modern/src/map/Map.js index fec8d501..8a43e97b 100644 --- a/modern/src/map/Map.js +++ b/modern/src/map/Map.js @@ -1,8 +1,13 @@ import 'mapbox-gl/dist/mapbox-gl.css'; +import './switcher/switcher.css'; import mapboxgl from 'mapbox-gl'; +import { SwitcherControl } from './switcher/switcher'; import React, { useRef, useLayoutEffect, useEffect, useState } from 'react'; import { deviceCategories } from '../common/deviceCategories'; import { loadIcon, loadImage } from './mapUtil'; +import { styleCarto, styleMapbox, styleOsm } from './mapStyles'; +import t from '../common/localization'; +import { useAttributePreference } from '../common/preferences'; const element = document.createElement('div'); element.style.width = '100%'; @@ -10,55 +15,83 @@ 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', - }], - }, + style: styleOsm(), }); -map.addControl(new mapboxgl.NavigationControl()); +let ready = false; +const readyListeners = new Set(); -let readyListeners = []; +const addReadyListener = listener => { + readyListeners.add(listener); + listener(ready); +}; -const onMapReady = listener => { - if (!readyListeners) { - listener(); - } else { - readyListeners.push(listener); - } +const removeReadyListener = listener => { + readyListeners.delete(listener); }; -map.on('load', async () => { +const updateReadyValue = value => { + ready = value; + readyListeners.forEach(listener => listener(value)); +}; + +const initMap = 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 (!map.hasImage(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; - } -}); + updateReadyValue(true); +}; + +map.on('load', initMap); + +map.addControl(new mapboxgl.NavigationControl({ + showCompass: false, +})); + +map.addControl(new SwitcherControl( + [ + { title: t('mapOsm'), uri: styleOsm() }, + { title: t('mapCarto'), uri: styleCarto() }, + { title: t('mapMapboxStreets'), uri: styleMapbox('streets-v11') }, + { title: t('mapMapboxOutdoors'), uri: styleMapbox('outdoors-v11') }, + { title: t('mapMapboxSatellite'), uri: styleMapbox('satellite-v9') }, + ], + t('mapOsm'), + () => updateReadyValue(false), + () => { + const waiting = () => { + if (!map.loaded()) { + setTimeout(waiting, 100); + } else { + initMap(); + } + }; + waiting(); + }, +)); const Map = ({ children }) => { const containerEl = useRef(null); const [mapReady, setMapReady] = useState(false); - - useEffect(() => onMapReady(() => setMapReady(true)), []); + + const mapboxAccessToken = useAttributePreference('mapboxAccessToken'); + + useEffect(() => { + mapboxgl.accessToken = mapboxAccessToken; + }, [mapboxAccessToken]); + + useEffect(() => { + const listener = ready => setMapReady(ready); + addReadyListener(listener); + return () => { + removeReadyListener(listener); + }; + }, []); useLayoutEffect(() => { const currentEl = containerEl.current; diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js index 6a7a68bb..fa7b431a 100644 --- a/modern/src/map/PositionsMap.js +++ b/modern/src/map/PositionsMap.js @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import ReactDOM from 'react-dom'; import mapboxgl from 'mapbox-gl'; import { Provider, useSelector } from 'react-redux'; @@ -8,36 +8,25 @@ import store from '../store'; import { useHistory } from 'react-router-dom'; import StatusView from './StatusView'; -const PositionsMap = () => { +const PositionsMap = ({ positions }) => { const id = 'positions'; const history = useHistory(); + const devices = useSelector(state => state.devices.items); - const createFeature = (state, position) => { - const device = state.devices.items[position.deviceId] || null; + const createFeature = (devices, position) => { + const device = devices[position.deviceId] || null; return { deviceId: position.deviceId, name: device ? device.name : '', - category: device && device.category || 'default', + 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 onClickCallback = useCallback(event => { const feature = event.features[0]; let coordinates = feature.geometry.coordinates.slice(); while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) { @@ -59,12 +48,15 @@ const PositionsMap = () => { .setDOMContent(placeholder) .setLngLat(coordinates) .addTo(map); - }; + }, [history]); useEffect(() => { map.addSource(id, { 'type': 'geojson', - 'data': positions, + 'data': { + type: 'FeatureCollection', + features: [], + } }); map.addLayer({ 'id': id, @@ -88,23 +80,33 @@ const PositionsMap = () => { map.on('mouseenter', id, onMouseEnter); map.on('mouseleave', id, onMouseLeave); - map.on('click', id, onClick); + map.on('click', id, onClickCallback); 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.off('click', id, onClickCallback); map.removeLayer(id); map.removeSource(id); }; - }, []); + }, [onClickCallback]); useEffect(() => { - map.getSource(id).setData(positions); - }, [positions]); + map.getSource(id).setData({ + type: 'FeatureCollection', + features: positions.map(position => ({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [position.longitude, position.latitude], + }, + properties: createFeature(devices, position), + })) + }); + }, [devices, positions]); return null; } diff --git a/modern/src/map/ReplayPathMap.js b/modern/src/map/ReplayPathMap.js new file mode 100644 index 00000000..feab0718 --- /dev/null +++ b/modern/src/map/ReplayPathMap.js @@ -0,0 +1,59 @@ +import mapboxgl from 'mapbox-gl'; +import { useEffect } from 'react'; +import { map } from './Map'; + +const ReplayPathMap = ({ positions }) => { + const id = 'replay'; + + useEffect(() => { + map.addSource(id, { + 'type': 'geojson', + 'data': { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [], + }, + }, + }); + map.addLayer({ + 'source': id, + 'id': id, + 'type': 'line', + 'layout': { + 'line-join': 'round', + 'line-cap': 'round', + }, + 'paint': { + 'line-color': '#333', + 'line-width': 5, + }, + }); + + return () => { + map.removeLayer(id); + map.removeSource(id); + }; + }, []); + + useEffect(() => { + const coordinates = positions.map(item => [item.longitude, item.latitude]); + map.getSource(id).setData({ + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: coordinates, + }, + }); + if (coordinates.length) { + const bounds = coordinates.reduce((bounds, item) => bounds.extend(item), new mapboxgl.LngLatBounds(coordinates[0], coordinates[0])); + map.fitBounds(bounds, { + padding: { top: 50, bottom: 250, left: 25, right: 25 }, + }); + } + }, [positions]); + + return null; +} + +export default ReplayPathMap; diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js index 3a304263..ae049af1 100644 --- a/modern/src/map/StatusView.js +++ b/modern/src/map/StatusView.js @@ -22,7 +22,7 @@ const StatusView = ({ deviceId, onShowDetails }) => { {position.attributes.batteryLevel && <><b>{t('positionBattery')}:</b> {formatPosition(position.attributes.batteryLevel, 'batteryLevel')}<br /></> } - <a href="#" onClick={handleClick}>{t('sharedShowDetails')}</a> + <a href="/" onClick={handleClick}>{t('sharedShowDetails')}</a> </> ); }; diff --git a/modern/src/map/mapStyles.js b/modern/src/map/mapStyles.js new file mode 100644 index 00000000..ff323cb3 --- /dev/null +++ b/modern/src/map/mapStyles.js @@ -0,0 +1,52 @@ +export const styleCustom = (url, attribution) => ({ + version: 8, + sources: { + osm: { + type: 'raster', + tiles: [url], + attribution: attribution, + }, + }, + glyphs: 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf', + layers: [{ + id: 'osm', + type: 'raster', + source: 'osm', + }], +}); + +export const styleOsm = () => styleCustom( + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', +); + +export const styleCarto = () => ({ + 'version': 8, + 'sources': { + 'raster-tiles': { + 'type': 'raster', + 'tiles': [ + 'https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://b.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://c.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png', + 'https://d.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}@2x.png' + ], + 'tileSize': 256, + 'attribution': '© <a target="_top" rel="noopener" href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a target="_top" rel="noopener" href="https://carto.com/attribution">CARTO</a>' + } + }, + 'glyphs': 'https://cdn.traccar.com/map/fonts/{fontstack}/{range}.pbf', + 'layers': [ + { + 'id': 'simple-tiles', + 'type': 'raster', + 'source': 'raster-tiles', + 'minzoom': 0, + 'maxzoom': 22, + } + ] +}); + +export const styleMapbox = (style) => `mapbox://styles/mapbox/${style}`; + +export const styleMapTiler = (style, key) => `https://api.maptiler.com/maps/${style}/style.json?key=${key}`; diff --git a/modern/src/map/mapUtil.js b/modern/src/map/mapUtil.js index 71d7b3c9..15f16202 100644 --- a/modern/src/map/mapUtil.js +++ b/modern/src/map/mapUtil.js @@ -27,30 +27,6 @@ export const loadIcon = async (key, background, url) => { return context.getImageData(0, 0, canvas.width, canvas.height); }; -export const calculateBounds = features => { - if (features && features.length) { - const first = features[0].geometry.coordinates; - const bounds = [[...first], [...first]]; - for (let feature of features) { - const longitude = feature.geometry.coordinates[0] - const latitude = feature.geometry.coordinates[1] - if (longitude < bounds[0][0]) { - bounds[0][0] = longitude; - } else if (longitude > bounds[1][0]) { - bounds[1][0] = longitude; - } - if (latitude < bounds[0][1]) { - bounds[0][1] = latitude; - } else if (latitude > bounds[1][1]) { - bounds[1][1] = latitude; - } - } - return bounds; - } else { - return null; - } -} - export const reverseCoordinates = it => { if (!it) { return it; diff --git a/modern/src/map/switcher/switcher.css b/modern/src/map/switcher/switcher.css new file mode 100644 index 00000000..6c8fdb4e --- /dev/null +++ b/modern/src/map/switcher/switcher.css @@ -0,0 +1,40 @@ +.mapboxgl-style-list +{ + display: none; +} + +.mapboxgl-ctrl-group .mapboxgl-style-list button +{ + background: none; + border: none; + cursor: pointer; + display: block; + font-size: 14px; + padding: 8px 8px 6px; + text-align: right; + width: 100%; + height: auto; +} + +.mapboxgl-style-list button.active +{ + font-weight: bold; +} + +.mapboxgl-style-list button:hover +{ + background-color: rgba(0, 0, 0, 0.05); +} + +.mapboxgl-style-list button + button +{ + border-top: 1px solid #ddd; +} + +.mapboxgl-style-switcher +{ + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iNTQuODQ5cHgiIGhlaWdodD0iNTQuODQ5cHgiIHZpZXdCb3g9IjAgMCA1NC44NDkgNTQuODQ5IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1NC44NDkgNTQuODQ5OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+PGc+PGc+PHBhdGggZD0iTTU0LjQ5NywzOS42MTRsLTEwLjM2My00LjQ5bC0xNC45MTcsNS45NjhjLTAuNTM3LDAuMjE0LTEuMTY1LDAuMzE5LTEuNzkzLDAuMzE5Yy0wLjYyNywwLTEuMjU0LTAuMTA0LTEuNzktMC4zMThsLTE0LjkyMS01Ljk2OEwwLjM1MSwzOS42MTRjLTAuNDcyLDAuMjAzLTAuNDY3LDAuNTI0LDAuMDEsMC43MTZMMjYuNTYsNTAuODFjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMEw1NC40ODgsNDAuMzNDNTQuOTY0LDQwLjEzOSw1NC45NjksMzkuODE3LDU0LjQ5NywzOS42MTR6Ii8+PHBhdGggZD0iTTU0LjQ5NywyNy41MTJsLTEwLjM2NC00LjQ5MWwtMTQuOTE2LDUuOTY2Yy0wLjUzNiwwLjIxNS0xLjE2NSwwLjMyMS0xLjc5MiwwLjMyMWMtMC42MjgsMC0xLjI1Ni0wLjEwNi0xLjc5My0wLjMyMWwtMTQuOTE4LTUuOTY2TDAuMzUxLDI3LjUxMmMtMC40NzIsMC4yMDMtMC40NjcsMC41MjMsMC4wMSwwLjcxNkwyNi41NiwzOC43MDZjMC40NzcsMC4xOSwxLjI1MSwwLjE5LDEuNzI5LDBsMjYuMTk5LTEwLjQ3OUM1NC45NjQsMjguMDM2LDU0Ljk2OSwyNy43MTYsNTQuNDk3LDI3LjUxMnoiLz48cGF0aCBkPSJNMC4zNjEsMTYuMTI1bDEzLjY2Miw1LjQ2NWwxMi41MzcsNS4wMTVjMC40NzcsMC4xOTEsMS4yNTEsMC4xOTEsMS43MjksMGwxMi41NDEtNS4wMTZsMTMuNjU4LTUuNDYzYzAuNDc3LTAuMTkxLDAuNDgtMC41MTEsMC4wMS0wLjcxNkwyOC4yNzcsNC4wNDhjLTAuNDcxLTAuMjA0LTEuMjM2LTAuMjA0LTEuNzA4LDBMMC4zNTEsMTUuNDFDLTAuMTIxLDE1LjYxNC0wLjExNiwxNS45MzUsMC4zNjEsMTYuMTI1eiIvPjwvZz48L2c+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjxnPjwvZz48Zz48L2c+PGc+PC9nPjwvc3ZnPg==); + background-position: center; + background-repeat: no-repeat; + background-size: 70%; +} diff --git a/modern/src/map/switcher/switcher.js b/modern/src/map/switcher/switcher.js new file mode 100644 index 00000000..ff9fbe97 --- /dev/null +++ b/modern/src/map/switcher/switcher.js @@ -0,0 +1,79 @@ +export class SwitcherControl { + + constructor(styles, defaultStyle, beforeSwitch, afterSwitch) { + this.styles = styles; + this.defaultStyle = defaultStyle; + this.beforeSwitch = beforeSwitch; + this.afterSwitch = afterSwitch; + this.onDocumentClick = this.onDocumentClick.bind(this); + } + + getDefaultPosition() { + return 'top-right'; + } + + onAdd(map) { + this.map = map; + this.controlContainer = document.createElement('div'); + this.controlContainer.classList.add('mapboxgl-ctrl'); + this.controlContainer.classList.add('mapboxgl-ctrl-group'); + this.mapStyleContainer = document.createElement('div'); + this.styleButton = document.createElement('button'); + this.styleButton.type = 'button'; + this.mapStyleContainer.classList.add('mapboxgl-style-list'); + for (const style of this.styles) { + const styleElement = document.createElement('button'); + styleElement.type = 'button'; + styleElement.innerText = style.title; + styleElement.classList.add(style.title.replace(/[^a-z0-9-]/gi, '_')); + styleElement.dataset.uri = JSON.stringify(style.uri); + styleElement.addEventListener('click', event => { + const srcElement = event.srcElement; + if (srcElement.classList.contains('active')) { + return; + } + this.beforeSwitch(); + this.map.setStyle(JSON.parse(srcElement.dataset.uri)); + this.afterSwitch(); + this.mapStyleContainer.style.display = 'none'; + this.styleButton.style.display = 'block'; + const elms = this.mapStyleContainer.getElementsByClassName('active'); + while (elms[0]) { + elms[0].classList.remove('active'); + } + srcElement.classList.add('active'); + }); + if (style.title === this.defaultStyle) { + styleElement.classList.add('active'); + } + this.mapStyleContainer.appendChild(styleElement); + } + this.styleButton.classList.add('mapboxgl-ctrl-icon'); + this.styleButton.classList.add('mapboxgl-style-switcher'); + this.styleButton.addEventListener('click', () => { + this.styleButton.style.display = 'none'; + this.mapStyleContainer.style.display = 'block'; + }); + document.addEventListener('click', this.onDocumentClick); + this.controlContainer.appendChild(this.styleButton); + this.controlContainer.appendChild(this.mapStyleContainer); + return this.controlContainer; + } + + onRemove() { + if (!this.controlContainer || !this.controlContainer.parentNode || !this.map || !this.styleButton) { + return; + } + this.styleButton.removeEventListener('click', this.onDocumentClick); + this.controlContainer.parentNode.removeChild(this.controlContainer); + document.removeEventListener('click', this.onDocumentClick); + this.map = undefined; + } + + onDocumentClick(event) { + if (this.controlContainer && !this.controlContainer.contains(event.target) && this.mapStyleContainer && this.styleButton) { + this.mapStyleContainer.style.display = 'none'; + this.styleButton.style.display = 'block'; + } + } +} |