aboutsummaryrefslogtreecommitdiff
path: root/src/map/core/MapView.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/map/core/MapView.jsx')
-rw-r--r--src/map/core/MapView.jsx123
1 files changed, 123 insertions, 0 deletions
diff --git a/src/map/core/MapView.jsx b/src/map/core/MapView.jsx
new file mode 100644
index 00000000..35b3a65a
--- /dev/null
+++ b/src/map/core/MapView.jsx
@@ -0,0 +1,123 @@
+import 'maplibre-gl/dist/maplibre-gl.css';
+import maplibregl from 'maplibre-gl';
+import React, {
+ useRef, useLayoutEffect, useEffect, useState,
+} from 'react';
+import { SwitcherControl } from '../switcher/switcher';
+import { useAttributePreference, usePreference } from '../../common/util/preferences';
+import usePersistedState, { savePersistedState } from '../../common/util/usePersistedState';
+import { mapImages } from './preloadImages';
+import useMapStyles from './useMapStyles';
+
+const element = document.createElement('div');
+element.style.width = '100%';
+element.style.height = '100%';
+element.style.boxSizing = 'initial';
+
+export const map = new maplibregl.Map({
+ container: element,
+ attributionControl: false,
+});
+
+let ready = false;
+const readyListeners = new Set();
+
+const addReadyListener = (listener) => {
+ readyListeners.add(listener);
+ listener(ready);
+};
+
+const removeReadyListener = (listener) => {
+ readyListeners.delete(listener);
+};
+
+const updateReadyValue = (value) => {
+ ready = value;
+ readyListeners.forEach((listener) => listener(value));
+};
+
+const initMap = async () => {
+ if (ready) return;
+ if (!map.hasImage('background')) {
+ Object.entries(mapImages).forEach(([key, value]) => {
+ map.addImage(key, value, {
+ pixelRatio: window.devicePixelRatio,
+ });
+ });
+ }
+ updateReadyValue(true);
+};
+
+map.addControl(new maplibregl.NavigationControl());
+
+const switcher = new SwitcherControl(
+ () => updateReadyValue(false),
+ (styleId) => savePersistedState('selectedMapStyle', styleId),
+ () => {
+ map.once('styledata', () => {
+ const waiting = () => {
+ if (!map.loaded()) {
+ setTimeout(waiting, 33);
+ } else {
+ initMap();
+ }
+ };
+ waiting();
+ });
+ },
+);
+
+map.addControl(switcher);
+
+const MapView = ({ children }) => {
+ const containerEl = useRef(null);
+
+ const [mapReady, setMapReady] = useState(false);
+
+ const mapStyles = useMapStyles();
+ const activeMapStyles = useAttributePreference('activeMapStyles', 'locationIqStreets,osm,carto');
+ const [defaultMapStyle] = usePersistedState('selectedMapStyle', usePreference('map', 'locationIqStreets'));
+ const mapboxAccessToken = useAttributePreference('mapboxAccessToken');
+ const maxZoom = useAttributePreference('web.maxZoom');
+
+ useEffect(() => {
+ if (maxZoom) {
+ map.setMaxZoom(maxZoom);
+ }
+ }, [maxZoom]);
+
+ useEffect(() => {
+ maplibregl.accessToken = mapboxAccessToken;
+ }, [mapboxAccessToken]);
+
+ useEffect(() => {
+ const filteredStyles = mapStyles.filter((s) => s.available && activeMapStyles.includes(s.id));
+ const styles = filteredStyles.length ? filteredStyles : mapStyles.filter((s) => s.id === 'osm');
+ switcher.updateStyles(styles, defaultMapStyle);
+ }, [mapStyles, defaultMapStyle]);
+
+ useEffect(() => {
+ const listener = (ready) => setMapReady(ready);
+ addReadyListener(listener);
+ return () => {
+ removeReadyListener(listener);
+ };
+ }, []);
+
+ useLayoutEffect(() => {
+ const currentEl = containerEl.current;
+ currentEl.appendChild(element);
+ map.resize();
+ return () => {
+ currentEl.removeChild(element);
+ };
+ }, [containerEl]);
+
+ return (
+ <div style={{ width: '100%', height: '100%' }} ref={containerEl}>
+ {mapReady && children}
+ </div>
+ );
+};
+
+export default MapView;