aboutsummaryrefslogtreecommitdiff
path: root/modern/src/map
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/map')
-rw-r--r--modern/src/map/AccuracyMap.js53
-rw-r--r--modern/src/map/CurrentLocationMap.js21
-rw-r--r--modern/src/map/CurrentPositionsMap.js11
-rw-r--r--modern/src/map/Map.js103
-rw-r--r--modern/src/map/PositionsMap.js52
-rw-r--r--modern/src/map/ReplayPathMap.js59
-rw-r--r--modern/src/map/StatusView.js2
-rw-r--r--modern/src/map/mapStyles.js52
-rw-r--r--modern/src/map/mapUtil.js24
-rw-r--r--modern/src/map/switcher/switcher.css40
-rw-r--r--modern/src/map/switcher/switcher.js79
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 0000000..e81fc8f
--- /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 0000000..31e6e28
--- /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 0000000..0bfe4fc
--- /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 fec8d50..8a43e97 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 6a7a68b..fa7b431 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 0000000..feab071
--- /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 3a30426..ae049af 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 0000000..ff323cb
--- /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 71d7b3c..15f1620 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 0000000..6c8fdb4
--- /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 0000000..ff9fbe9
--- /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';
+ }
+ }
+}