From 70dc651230720ac03199b3b2a0ca6a5fb43f3c3d Mon Sep 17 00:00:00 2001 From: htlee Date: Sun, 15 Feb 2026 15:48:49 +0900 Subject: [PATCH] Keep globe overlays stable and reuse globe layer IDs --- apps/web/src/widgets/map3d/Map3D.tsx | 133 +++++++++++++++++---------- 1 file changed, 83 insertions(+), 50 deletions(-) diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index 2c1a343..f1f431d 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -121,6 +121,26 @@ function getZoneDisplayNameFromProps(props: Record | null | und return ZONE_META[(zoneId as ZoneId)]?.name || `수역 ${zoneId}`; } +function makeOrderedPairKey(a: number, b: number) { + const left = Math.trunc(Math.min(a, b)); + const right = Math.trunc(Math.max(a, b)); + return `${left}-${right}`; +} + +function makePairLinkFeatureId(a: number, b: number, suffix?: string) { + const pair = makeOrderedPairKey(a, b); + return suffix ? `pair-${pair}-${suffix}` : `pair-${pair}`; +} + +function makeFcSegmentFeatureId(a: number, b: number, segmentIndex: number) { + const pair = makeOrderedPairKey(a, b); + return `fc-${pair}-${segmentIndex}`; +} + +function makeFleetCircleFeatureId(ownerKey: string) { + return `fleet-${ownerKey}`; +} + const SHIP_ICON_MAPPING = { ship: { x: 0, @@ -972,20 +992,21 @@ export function Map3D({ return keys; }, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]); - const reorderGlobeFeatureLayers = useCallback(() => { + const reorderGlobeFeatureLayers = useCallback((options?: { shipTop?: boolean }) => { const map = mapRef.current; if (!map || projectionRef.current !== "globe") return; if (projectionBusyRef.current) return; + const shipTop = options?.shipTop === true; const ordering = [ + "zones-fill", + "zones-line", + "zones-label", "pair-lines-ml", "fc-lines-ml", "pair-range-ml", "fleet-circles-ml-fill", "fleet-circles-ml", - "zones-fill", - "zones-line", - "zones-label", "ships-globe-halo", "ships-globe-outline", "ships-globe", @@ -999,6 +1020,17 @@ export function Map3D({ } } + if (!shipTop) return; + + const shipOrdering = ["ships-globe-halo", "ships-globe-outline", "ships-globe"]; + for (const layerId of shipOrdering) { + try { + if (map.getLayer(layerId)) map.moveLayer(layerId); + } catch { + // ignore + } + } + kickRepaint(map); }, []); @@ -2405,7 +2437,7 @@ export function Map3D({ // Selection and highlight are now source-data driven. bringShipLayersToFront(); - reorderGlobeFeatureLayers(); + reorderGlobeFeatureLayers({ shipTop: true }); kickRepaint(map); }; @@ -2514,12 +2546,7 @@ export function Map3D({ const remove = () => { try { - if (map.getLayer(layerId)) map.removeLayer(layerId); - } catch { - // ignore - } - try { - if (map.getSource(srcId)) map.removeSource(srcId); + if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none"); } catch { // ignore } @@ -2535,9 +2562,9 @@ export function Map3D({ const fc: GeoJSON.FeatureCollection = { type: "FeatureCollection", - features: (pairLinks || []).map((p, idx) => ({ + features: (pairLinks || []).map((p) => ({ type: "Feature", - id: `${p.aMmsi}-${p.bMmsi}-${idx}`, + id: makePairLinkFeatureId(p.aMmsi, p.bMmsi), geometry: { type: "LineString", coordinates: [p.from, p.to] }, properties: { type: "pair", @@ -2588,12 +2615,18 @@ export function Map3D({ ] as never, "line-opacity": 0.9, }, - } as unknown as LayerSpecification, + } as unknown as LayerSpecification, before, ); } catch (e) { console.warn("Pair lines layer add failed:", e); } + } else { + try { + map.setLayoutProperty(layerId, "visibility", "visible"); + } catch { + // ignore + } } reorderGlobeFeatureLayers(); @@ -2604,7 +2637,6 @@ export function Map3D({ ensure(); return () => { stop(); - remove(); }; }, [ projection, @@ -2626,12 +2658,7 @@ export function Map3D({ const remove = () => { try { - if (map.getLayer(layerId)) map.removeLayer(layerId); - } catch { - // ignore - } - try { - if (map.getSource(srcId)) map.removeSource(srcId); + if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none"); } catch { // ignore } @@ -2658,7 +2685,7 @@ export function Map3D({ type: "FeatureCollection", features: segs.map((s, idx) => ({ type: "Feature", - id: `fc-${idx}`, + id: makeFcSegmentFeatureId(s.fromMmsi ?? -1, s.toMmsi ?? -1, idx), geometry: { type: "LineString", coordinates: [s.from, s.to] }, properties: { type: "fc", @@ -2702,12 +2729,18 @@ export function Map3D({ "line-width": ["case", ["==", ["get", "highlighted"], 1], 2.0, 1.3] as never, "line-opacity": 0.9, }, - } as unknown as LayerSpecification, + } as unknown as LayerSpecification, before, ); } catch (e) { console.warn("FC lines layer add failed:", e); } + } else { + try { + map.setLayoutProperty(layerId, "visibility", "visible"); + } catch { + // ignore + } } reorderGlobeFeatureLayers(); @@ -2718,7 +2751,6 @@ export function Map3D({ ensure(); return () => { stop(); - remove(); }; }, [ projection, @@ -2741,22 +2773,12 @@ export function Map3D({ const remove = () => { try { - if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId); + if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, "visibility", "none"); } catch { // ignore } try { - if (map.getLayer(layerId)) map.removeLayer(layerId); - } catch { - // ignore - } - try { - if (map.getSource(srcId)) map.removeSource(srcId); - } catch { - // ignore - } - try { - if (map.getSource(fillSrcId)) map.removeSource(fillSrcId); + if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none"); } catch { // ignore } @@ -2772,11 +2794,11 @@ export function Map3D({ const fcLine: GeoJSON.FeatureCollection = { type: "FeatureCollection", - features: (fleetCircles || []).map((c, idx) => { + features: (fleetCircles || []).map((c) => { const ring = circleRingLngLat(c.center, c.radiusNm * 1852); return { type: "Feature", - id: `fleet-${c.ownerKey}-${idx}`, + id: makeFleetCircleFeatureId(c.ownerKey), geometry: { type: "LineString", coordinates: ring }, properties: { type: "fleet", @@ -2795,11 +2817,11 @@ export function Map3D({ const fcFill: GeoJSON.FeatureCollection = { type: "FeatureCollection", - features: (fleetCircles || []).map((c, idx) => { + features: (fleetCircles || []).map((c) => { const ring = circleRingLngLat(c.center, c.radiusNm * 1852); return { type: "Feature", - id: `fleet-fill-${c.ownerKey}-${idx}`, + id: `${makeFleetCircleFeatureId(c.ownerKey)}-fill`, geometry: { type: "Polygon", coordinates: [ring] }, properties: { type: "fleet-fill", @@ -2859,6 +2881,12 @@ export function Map3D({ } catch (e) { console.warn("Fleet circles fill layer add failed:", e); } + } else { + try { + map.setLayoutProperty(fillLayerId, "visibility", "visible"); + } catch { + // ignore + } } if (!map.getLayer(layerId)) { @@ -2880,6 +2908,12 @@ export function Map3D({ } catch (e) { console.warn("Fleet circles layer add failed:", e); } + } else { + try { + map.setLayoutProperty(layerId, "visibility", "visible"); + } catch { + // ignore + } } reorderGlobeFeatureLayers(); @@ -2890,7 +2924,6 @@ export function Map3D({ ensure(); return () => { stop(); - remove(); }; }, [ projection, @@ -2914,12 +2947,7 @@ export function Map3D({ const remove = () => { try { - if (map.getLayer(layerId)) map.removeLayer(layerId); - } catch { - // ignore - } - try { - if (map.getSource(srcId)) map.removeSource(srcId); + if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none"); } catch { // ignore } @@ -2952,11 +2980,11 @@ export function Map3D({ const fc: GeoJSON.FeatureCollection = { type: "FeatureCollection", - features: ranges.map((c, idx) => { + features: ranges.map((c) => { const ring = circleRingLngLat(c.center, c.radiusNm * 1852); return { type: "Feature", - id: `pair-range-${idx}`, + id: makePairLinkFeatureId(c.aMmsi, c.bMmsi), geometry: { type: "LineString", coordinates: ring }, properties: { type: "pair-range", @@ -3007,6 +3035,12 @@ export function Map3D({ } catch (e) { console.warn("Pair range layer add failed:", e); } + } else { + try { + map.setLayoutProperty(layerId, "visibility", "visible"); + } catch { + // ignore + } } kickRepaint(map); @@ -3016,7 +3050,6 @@ export function Map3D({ ensure(); return () => { stop(); - remove(); }; }, [ projection,