diff --git a/apps/web/src/widgets/map3d/Map3D.tsx b/apps/web/src/widgets/map3d/Map3D.tsx index ad62098..2c1a343 100644 --- a/apps/web/src/widgets/map3d/Map3D.tsx +++ b/apps/web/src/widgets/map3d/Map3D.tsx @@ -83,6 +83,44 @@ function makeSetSignature(values: Set) { return Array.from(values).sort((a, b) => a - b).join(","); } +function toTextValue(value: unknown): string { + if (value == null) return ""; + return String(value).trim(); +} + +function getZoneIdFromProps(props: Record | null | undefined): string { + const safeProps = props || {}; + const candidates = [ + "zoneId", + "zone_id", + "zoneIdNo", + "zoneKey", + "zoneCode", + "ZONE_ID", + "ZONECODE", + "id", + ]; + + for (const key of candidates) { + const value = toTextValue(safeProps[key]); + if (value) return value; + } + + return ""; +} + +function getZoneDisplayNameFromProps(props: Record | null | undefined): string { + const safeProps = props || {}; + const nameCandidates = ["zoneName", "zoneLabel", "NAME", "name", "ZONE_NM", "label"]; + for (const key of nameCandidates) { + const name = toTextValue(safeProps[key]); + if (name) return name; + } + const zoneId = getZoneIdFromProps(safeProps); + if (!zoneId) return "수역"; + return ZONE_META[(zoneId as ZoneId)]?.name || `수역 ${zoneId}`; +} + const SHIP_ICON_MAPPING = { ship: { x: 0, @@ -933,6 +971,37 @@ export function Map3D({ if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey); return keys; }, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]); + + const reorderGlobeFeatureLayers = useCallback(() => { + const map = mapRef.current; + if (!map || projectionRef.current !== "globe") return; + if (projectionBusyRef.current) return; + + const ordering = [ + "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", + ]; + + for (const layerId of ordering) { + try { + if (map.getLayer(layerId)) map.moveLayer(layerId); + } catch { + // ignore + } + } + + kickRepaint(map); + }, []); + const effectiveHoveredPairMmsiSet = useMemo( () => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef), [hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef], @@ -1515,6 +1584,7 @@ export function Map3D({ // MapLibre may not schedule a frame immediately after projection swaps if the map is idle. // Kick a few repaints so overlay sources (ships/zones) appear instantly. + reorderGlobeFeatureLayers(); kickRepaint(map); try { map.resize(); @@ -1545,7 +1615,7 @@ export function Map3D({ if (settleCleanup) settleCleanup(); if (isTransition) setProjectionLoading(false); }; - }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, setProjectionLoading]); + }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, reorderGlobeFeatureLayers, setProjectionLoading]); // Base map toggle useEffect(() => { @@ -1741,6 +1811,19 @@ export function Map3D({ hoveredZoneId !== null ? (["==", ["to-string", ["coalesce", ["get", "zoneId"], ""]], hoveredZoneId] as unknown[]) : false; + const zoneLineWidthExpr = hoveredZoneId + ? ([ + "interpolate", + ["linear"], + ["zoom"], + 4, + ["case", zoneMatchExpr, 1.6, 0.8], + 10, + ["case", zoneMatchExpr, 2.0, 1.4], + 14, + ["case", zoneMatchExpr, 2.8, 2.1], + ] as unknown as never) + : (["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1] as never); if (map.getLayer(fillId)) { try { @@ -1772,18 +1855,7 @@ export function Map3D({ // ignore } try { - map.setPaintProperty( - lineId, - "line-width", - hoveredZoneId - ? ([ - "case", - zoneMatchExpr, - ["interpolate", ["linear"], ["zoom"], 4, 1.6, 10, 2.0, 14, 2.8], - ["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1], - ] as never) - : (["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1] as never), - ); + map.setPaintProperty(lineId, "line-width", zoneLineWidthExpr); } catch { // ignore } @@ -1825,14 +1897,7 @@ export function Map3D({ "line-opacity": hoveredZoneId ? (["case", zoneMatchExpr, 1, 0.85] as never) : 0.85, - "line-width": hoveredZoneId - ? ([ - "case", - zoneMatchExpr, - ["interpolate", ["linear"], ["zoom"], 4, 1.6, 10, 2.0, 14, 2.8], - ["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1], - ] as unknown as number[]) - : (["interpolate", ["linear"], ["zoom"], 4, 0.8, 10, 1.4, 14, 2.1] as never), + "line-width": zoneLineWidthExpr, }, layout: { visibility }, } as unknown as LayerSpecification, @@ -1870,6 +1935,7 @@ export function Map3D({ } catch (e) { console.warn("Zones layer setup failed:", e); } finally { + reorderGlobeFeatureLayers(); kickRepaint(map); } }; @@ -1878,7 +1944,7 @@ export function Map3D({ return () => { stop(); }; - }, [zones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch]); + }, [zones, overlays.zones, projection, baseMap, hoveredZoneId, mapSyncEpoch, reorderGlobeFeatureLayers]); // Ships in globe mode: render with MapLibre symbol layers so icons can be aligned to the globe surface. // Deck IconLayer billboards or uses a fixed plane, which looks like ships are pointing into the sky/ground on globe. @@ -1905,6 +1971,7 @@ export function Map3D({ } catch { // ignore } + reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -2338,6 +2405,7 @@ export function Map3D({ // Selection and highlight are now source-data driven. bringShipLayersToFront(); + reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -2345,7 +2413,19 @@ export function Map3D({ return () => { stop(); }; - }, [projection, settings.showShips, targets, legacyHits, selectedMmsi, hoveredMmsiSetRef, hoveredFleetMmsiSetRef, hoveredPairMmsiSetRef, isHighlightedMmsi, mapSyncEpoch]); + }, [ + projection, + settings.showShips, + targets, + legacyHits, + selectedMmsi, + hoveredMmsiSetRef, + hoveredFleetMmsiSetRef, + hoveredPairMmsiSetRef, + isHighlightedMmsi, + mapSyncEpoch, + reorderGlobeFeatureLayers, + ]); // Globe ship click selection (MapLibre-native ships layer) useEffect(() => { @@ -2516,6 +2596,7 @@ export function Map3D({ } } + reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -2525,7 +2606,16 @@ export function Map3D({ stop(); remove(); }; - }, [projection, overlays.pairLines, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, isHighlightedPair]); + }, [ + projection, + overlays.pairLines, + pairLinks, + mapSyncEpoch, + hoveredShipSignature, + hoveredPairSignature, + isHighlightedPair, + reorderGlobeFeatureLayers, + ]); useEffect(() => { const map = mapRef.current; @@ -2620,6 +2710,7 @@ export function Map3D({ } } + reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -2629,7 +2720,15 @@ export function Map3D({ stop(); remove(); }; - }, [projection, overlays.fcLines, fcLinks, mapSyncEpoch, hoveredShipSignature, isHighlightedMmsi]); + }, [ + projection, + overlays.fcLines, + fcLinks, + mapSyncEpoch, + hoveredShipSignature, + isHighlightedMmsi, + reorderGlobeFeatureLayers, + ]); useEffect(() => { const map = mapRef.current; @@ -2783,6 +2882,7 @@ export function Map3D({ } } + reorderGlobeFeatureLayers(); kickRepaint(map); }; @@ -2802,6 +2902,7 @@ export function Map3D({ hoveredFleetOwnerKey, isHighlightedFleet, isHighlightedMmsi, + reorderGlobeFeatureLayers, ]); useEffect(() => { @@ -2917,7 +3018,17 @@ export function Map3D({ stop(); remove(); }; - }, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, hoveredFleetSignature, isHighlightedPair]); + }, [ + projection, + overlays.pairRange, + pairLinks, + mapSyncEpoch, + hoveredShipSignature, + hoveredPairSignature, + hoveredFleetSignature, + isHighlightedPair, + reorderGlobeFeatureLayers, + ]); const shipData = useMemo(() => { return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi)); @@ -3019,10 +3130,9 @@ export function Map3D({ }); } - const zoneLabel = String((props.zoneLabel ?? props.zoneName ?? "").toString()); + const zoneLabel = getZoneDisplayNameFromProps(props); if (zoneLabel) { - const zoneName = zoneLabel || ZONE_META[(String(props.zoneId ?? "") as ZoneId)]?.name || "수역"; - return { html: `
${zoneName}
` }; + return { html: `
${zoneLabel}
` }; } return null; @@ -3174,7 +3284,7 @@ export function Map3D({ clearMapFleetHoverState(); setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : [])); setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : [])); - const zoneId = String((props.zoneId ?? "").toString()); + const zoneId = getZoneIdFromProps(props); setHoveredZoneId(zoneId || null); } else { resetGlobeHoverStates(); @@ -3761,8 +3871,8 @@ export function Map3D({ }); } - const p = obj.properties as { zoneName?: string; zoneLabel?: string } | undefined; - const label = p?.zoneName ?? p?.zoneLabel; + const p = obj.properties as Record | undefined; + const label = getZoneDisplayNameFromProps(p); if (label) return { text: label }; return null; },