diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index a2487b0..0afa504 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -9,7 +9,7 @@ import maplibregl, { type StyleSpecification, type VectorSourceSpecification, } from "maplibre-gl"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { AisTarget } from "../../entities/aisTarget/model/types"; import type { LegacyVesselInfo } from "../../entities/legacyVessel/model/types"; import type { ZonesGeoJson } from "../../entities/zone/api/useZones"; @@ -580,6 +580,67 @@ export function Map3D({ projectionRef.current = projection; }, [projection]); + const removeLayerIfExists = useCallback((map: maplibregl.Map, layerId: string) => { + try { + if (map.getLayer(layerId)) { + map.removeLayer(layerId); + } + } catch { + // ignore + } + }, []); + + const removeSourceIfExists = useCallback((map: maplibregl.Map, sourceId: string) => { + try { + if (map.getSource(sourceId)) { + map.removeSource(sourceId); + } + } catch { + // ignore + } + }, []); + + const ensureMercatorOverlay = useCallback(() => { + const map = mapRef.current; + if (!map) return null; + if (overlayRef.current) return overlayRef.current; + + try { + const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never); + map.addControl(next); + overlayRef.current = next; + return next; + } catch (e) { + console.warn("Deck overlay create failed:", e); + return null; + } + }, []); + + const clearGlobeNativeLayers = useCallback(() => { + const map = mapRef.current; + if (!map) return; + + const layerIds = [ + "ships-globe-halo", + "ships-globe-outline", + "ships-globe", + "pair-lines-ml", + "fc-lines-ml", + "fleet-circles-ml", + "pair-range-ml", + "deck-globe", + ]; + + for (const id of layerIds) { + removeLayerIfExists(map, id); + } + + const sourceIds = ["ships-globe-src", "pair-lines-ml-src", "fc-lines-ml-src", "fleet-circles-ml-src", "pair-range-ml-src"]; + for (const id of sourceIds) { + removeSourceIfExists(map, id); + } + }, [removeLayerIfExists, removeSourceIfExists]); + // Init MapLibre + Deck.gl (single WebGL context via MapboxOverlay) useEffect(() => { if (!containerRef.current || mapRef.current) return; @@ -758,13 +819,7 @@ export function Map3D({ const disposeGlobeDeckLayer = () => { const current = globeDeckLayerRef.current; if (!current) return; - if (map.getLayer(current.id)) { - try { - map.removeLayer(current.id); - } catch { - // ignore - } - } + removeLayerIfExists(map, current.id); try { current.requestFinalize(); } catch { @@ -790,8 +845,10 @@ export function Map3D({ if (projection === "globe") { disposeMercatorOverlay(); + clearGlobeNativeLayers(); } else { disposeGlobeDeckLayer(); + clearGlobeNativeLayers(); } try { @@ -842,15 +899,7 @@ export function Map3D({ // Tear down globe custom layer (if present), restore MapboxOverlay interleaved. disposeGlobeDeckLayer(); - if (!overlayRef.current) { - try { - const next = new MapboxOverlay({ interleaved: true, layers: [] } as unknown as never); - map.addControl(next); - overlayRef.current = next; - } catch (e) { - console.warn("Deck overlay create failed:", e); - } - } + ensureMercatorOverlay(); } // MapLibre may not schedule a frame immediately after projection swaps if the map is idle. @@ -876,7 +925,7 @@ export function Map3D({ return () => { cancelled = true; }; - }, [projection]); + }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists]); // Base map toggle useEffect(() => { @@ -1333,7 +1382,7 @@ export function Map3D({ "icon-rotate": ["to-number", ["get", "cog"], 0], // Keep the icon on the sea surface. "icon-rotation-alignment": "map", - "icon-pitch-alignment": "viewport", + "icon-pitch-alignment": "map", }, paint: { "icon-color": ["coalesce", ["get", "shipColor"], "#64748b"] as never, @@ -1830,7 +1879,19 @@ export function Map3D({ useEffect(() => { const map = mapRef.current; if (!map) return; - const deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current; + let deckTarget = projection === "globe" ? globeDeckLayerRef.current : overlayRef.current; + + if (projection === "mercator") { + if (!deckTarget) deckTarget = ensureMercatorOverlay(); + if (!deckTarget) return; + try { + deckTarget.setProps({ layers: [] } as never); + } catch { + // ignore + } + } else if (!deckTarget && projection === "globe") { + return; + } if (!deckTarget) return; const overlayParams = projection === "globe" ? GLOBE_OVERLAY_PARAMS : DEPTH_DISABLED_PARAMS; @@ -2068,6 +2129,7 @@ export function Map3D({ fcDashed, fleetCircles, mapSyncEpoch, + ensureMercatorOverlay, ]); return
;