From f418231b6b2f5e030a0d2dcc390c314602b1f740 Mon Sep 17 00:00:00 2001
From: Anton Tananaev <anton@traccar.org>
Date: Sat, 6 Apr 2024 09:22:10 -0700
Subject: Move modern to the top

---
 src/map/core/MapView.jsx | 123 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 123 insertions(+)
 create mode 100644 src/map/core/MapView.jsx

(limited to 'src/map/core/MapView.jsx')

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;
-- 
cgit v1.2.3

