Fix globe zones line-width expression and enforce map layer ordering

This commit is contained in:
htlee 2026-02-15 15:43:36 +09:00
부모 bb5fd793d8
커밋 e504dbebca

파일 보기

@ -83,6 +83,44 @@ function makeSetSignature(values: Set<number>) {
return Array.from(values).sort((a, b) => a - b).join(","); 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<string, unknown> | 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<string, unknown> | 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 = { const SHIP_ICON_MAPPING = {
ship: { ship: {
x: 0, x: 0,
@ -933,6 +971,37 @@ export function Map3D({
if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey); if (hoveredDeckFleetOwnerKey) keys.add(hoveredDeckFleetOwnerKey);
return keys; return keys;
}, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]); }, [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( const effectiveHoveredPairMmsiSet = useMemo(
() => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef), () => mergeNumberSets(hoveredPairMmsiSetRef, hoveredDeckPairMmsiSetRef),
[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. // 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. // Kick a few repaints so overlay sources (ships/zones) appear instantly.
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
try { try {
map.resize(); map.resize();
@ -1545,7 +1615,7 @@ export function Map3D({
if (settleCleanup) settleCleanup(); if (settleCleanup) settleCleanup();
if (isTransition) setProjectionLoading(false); if (isTransition) setProjectionLoading(false);
}; };
}, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, setProjectionLoading]); }, [projection, clearGlobeNativeLayers, ensureMercatorOverlay, removeLayerIfExists, reorderGlobeFeatureLayers, setProjectionLoading]);
// Base map toggle // Base map toggle
useEffect(() => { useEffect(() => {
@ -1741,6 +1811,19 @@ export function Map3D({
hoveredZoneId !== null hoveredZoneId !== null
? (["==", ["to-string", ["coalesce", ["get", "zoneId"], ""]], hoveredZoneId] as unknown[]) ? (["==", ["to-string", ["coalesce", ["get", "zoneId"], ""]], hoveredZoneId] as unknown[])
: false; : 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)) { if (map.getLayer(fillId)) {
try { try {
@ -1772,18 +1855,7 @@ export function Map3D({
// ignore // ignore
} }
try { try {
map.setPaintProperty( map.setPaintProperty(lineId, "line-width", zoneLineWidthExpr);
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),
);
} catch { } catch {
// ignore // ignore
} }
@ -1825,14 +1897,7 @@ export function Map3D({
"line-opacity": hoveredZoneId "line-opacity": hoveredZoneId
? (["case", zoneMatchExpr, 1, 0.85] as never) ? (["case", zoneMatchExpr, 1, 0.85] as never)
: 0.85, : 0.85,
"line-width": hoveredZoneId "line-width": zoneLineWidthExpr,
? ([
"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),
}, },
layout: { visibility }, layout: { visibility },
} as unknown as LayerSpecification, } as unknown as LayerSpecification,
@ -1870,6 +1935,7 @@ export function Map3D({
} catch (e) { } catch (e) {
console.warn("Zones layer setup failed:", e); console.warn("Zones layer setup failed:", e);
} finally { } finally {
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
} }
}; };
@ -1878,7 +1944,7 @@ export function Map3D({
return () => { return () => {
stop(); 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. // 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. // 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 { } catch {
// ignore // ignore
} }
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
}; };
@ -2338,6 +2405,7 @@ export function Map3D({
// Selection and highlight are now source-data driven. // Selection and highlight are now source-data driven.
bringShipLayersToFront(); bringShipLayersToFront();
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
}; };
@ -2345,7 +2413,19 @@ export function Map3D({
return () => { return () => {
stop(); 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) // Globe ship click selection (MapLibre-native ships layer)
useEffect(() => { useEffect(() => {
@ -2516,6 +2596,7 @@ export function Map3D({
} }
} }
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
}; };
@ -2525,7 +2606,16 @@ export function Map3D({
stop(); stop();
remove(); remove();
}; };
}, [projection, overlays.pairLines, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, isHighlightedPair]); }, [
projection,
overlays.pairLines,
pairLinks,
mapSyncEpoch,
hoveredShipSignature,
hoveredPairSignature,
isHighlightedPair,
reorderGlobeFeatureLayers,
]);
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
@ -2620,6 +2710,7 @@ export function Map3D({
} }
} }
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
}; };
@ -2629,7 +2720,15 @@ export function Map3D({
stop(); stop();
remove(); remove();
}; };
}, [projection, overlays.fcLines, fcLinks, mapSyncEpoch, hoveredShipSignature, isHighlightedMmsi]); }, [
projection,
overlays.fcLines,
fcLinks,
mapSyncEpoch,
hoveredShipSignature,
isHighlightedMmsi,
reorderGlobeFeatureLayers,
]);
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
@ -2783,6 +2882,7 @@ export function Map3D({
} }
} }
reorderGlobeFeatureLayers();
kickRepaint(map); kickRepaint(map);
}; };
@ -2802,6 +2902,7 @@ export function Map3D({
hoveredFleetOwnerKey, hoveredFleetOwnerKey,
isHighlightedFleet, isHighlightedFleet,
isHighlightedMmsi, isHighlightedMmsi,
reorderGlobeFeatureLayers,
]); ]);
useEffect(() => { useEffect(() => {
@ -2917,7 +3018,17 @@ export function Map3D({
stop(); stop();
remove(); remove();
}; };
}, [projection, overlays.pairRange, pairLinks, mapSyncEpoch, hoveredShipSignature, hoveredPairSignature, hoveredFleetSignature, isHighlightedPair]); }, [
projection,
overlays.pairRange,
pairLinks,
mapSyncEpoch,
hoveredShipSignature,
hoveredPairSignature,
hoveredFleetSignature,
isHighlightedPair,
reorderGlobeFeatureLayers,
]);
const shipData = useMemo(() => { const shipData = useMemo(() => {
return targets.filter((t) => isFiniteNumber(t.lat) && isFiniteNumber(t.lon) && isFiniteNumber(t.mmsi)); 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) { if (zoneLabel) {
const zoneName = zoneLabel || ZONE_META[(String(props.zoneId ?? "") as ZoneId)]?.name || "수역"; return { html: `<div style="font-size: 12px; font-family: system-ui;">${zoneLabel}</div>` };
return { html: `<div style="font-size: 12px; font-family: system-ui;">${zoneName}</div>` };
} }
return null; return null;
@ -3174,7 +3284,7 @@ export function Map3D({
clearMapFleetHoverState(); clearMapFleetHoverState();
setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : [])); setHoveredDeckMmsiSet((prev) => (prev.length === 0 ? prev : []));
setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : [])); setHoveredDeckPairMmsiSet((prev) => (prev.length === 0 ? prev : []));
const zoneId = String((props.zoneId ?? "").toString()); const zoneId = getZoneIdFromProps(props);
setHoveredZoneId(zoneId || null); setHoveredZoneId(zoneId || null);
} else { } else {
resetGlobeHoverStates(); resetGlobeHoverStates();
@ -3761,8 +3871,8 @@ export function Map3D({
}); });
} }
const p = obj.properties as { zoneName?: string; zoneLabel?: string } | undefined; const p = obj.properties as Record<string, unknown> | undefined;
const label = p?.zoneName ?? p?.zoneLabel; const label = getZoneDisplayNameFromProps(p);
if (label) return { text: label }; if (label) return { text: label };
return null; return null;
}, },