Keep globe overlays stable and reuse globe layer IDs

This commit is contained in:
htlee 2026-02-15 15:48:49 +09:00
부모 e504dbebca
커밋 70dc651230

파일 보기

@ -121,6 +121,26 @@ function getZoneDisplayNameFromProps(props: Record<string, unknown> | null | und
return ZONE_META[(zoneId as ZoneId)]?.name || `수역 ${zoneId}`; 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 = { const SHIP_ICON_MAPPING = {
ship: { ship: {
x: 0, x: 0,
@ -972,20 +992,21 @@ export function Map3D({
return keys; return keys;
}, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]); }, [hoveredFleetOwnerKey, hoveredDeckFleetOwnerKey]);
const reorderGlobeFeatureLayers = useCallback(() => { const reorderGlobeFeatureLayers = useCallback((options?: { shipTop?: boolean }) => {
const map = mapRef.current; const map = mapRef.current;
if (!map || projectionRef.current !== "globe") return; if (!map || projectionRef.current !== "globe") return;
if (projectionBusyRef.current) return; if (projectionBusyRef.current) return;
const shipTop = options?.shipTop === true;
const ordering = [ const ordering = [
"zones-fill",
"zones-line",
"zones-label",
"pair-lines-ml", "pair-lines-ml",
"fc-lines-ml", "fc-lines-ml",
"pair-range-ml", "pair-range-ml",
"fleet-circles-ml-fill", "fleet-circles-ml-fill",
"fleet-circles-ml", "fleet-circles-ml",
"zones-fill",
"zones-line",
"zones-label",
"ships-globe-halo", "ships-globe-halo",
"ships-globe-outline", "ships-globe-outline",
"ships-globe", "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); kickRepaint(map);
}, []); }, []);
@ -2405,7 +2437,7 @@ export function Map3D({
// Selection and highlight are now source-data driven. // Selection and highlight are now source-data driven.
bringShipLayersToFront(); bringShipLayersToFront();
reorderGlobeFeatureLayers(); reorderGlobeFeatureLayers({ shipTop: true });
kickRepaint(map); kickRepaint(map);
}; };
@ -2514,12 +2546,7 @@ export function Map3D({
const remove = () => { const remove = () => {
try { try {
if (map.getLayer(layerId)) map.removeLayer(layerId); if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
} catch {
// ignore
}
try {
if (map.getSource(srcId)) map.removeSource(srcId);
} catch { } catch {
// ignore // ignore
} }
@ -2535,9 +2562,9 @@ export function Map3D({
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = { const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: "FeatureCollection", type: "FeatureCollection",
features: (pairLinks || []).map((p, idx) => ({ features: (pairLinks || []).map((p) => ({
type: "Feature", type: "Feature",
id: `${p.aMmsi}-${p.bMmsi}-${idx}`, id: makePairLinkFeatureId(p.aMmsi, p.bMmsi),
geometry: { type: "LineString", coordinates: [p.from, p.to] }, geometry: { type: "LineString", coordinates: [p.from, p.to] },
properties: { properties: {
type: "pair", type: "pair",
@ -2588,12 +2615,18 @@ export function Map3D({
] as never, ] as never,
"line-opacity": 0.9, "line-opacity": 0.9,
}, },
} as unknown as LayerSpecification, } as unknown as LayerSpecification,
before, before,
); );
} catch (e) { } catch (e) {
console.warn("Pair lines layer add failed:", e); console.warn("Pair lines layer add failed:", e);
} }
} else {
try {
map.setLayoutProperty(layerId, "visibility", "visible");
} catch {
// ignore
}
} }
reorderGlobeFeatureLayers(); reorderGlobeFeatureLayers();
@ -2604,7 +2637,6 @@ export function Map3D({
ensure(); ensure();
return () => { return () => {
stop(); stop();
remove();
}; };
}, [ }, [
projection, projection,
@ -2626,12 +2658,7 @@ export function Map3D({
const remove = () => { const remove = () => {
try { try {
if (map.getLayer(layerId)) map.removeLayer(layerId); if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
} catch {
// ignore
}
try {
if (map.getSource(srcId)) map.removeSource(srcId);
} catch { } catch {
// ignore // ignore
} }
@ -2658,7 +2685,7 @@ export function Map3D({
type: "FeatureCollection", type: "FeatureCollection",
features: segs.map((s, idx) => ({ features: segs.map((s, idx) => ({
type: "Feature", type: "Feature",
id: `fc-${idx}`, id: makeFcSegmentFeatureId(s.fromMmsi ?? -1, s.toMmsi ?? -1, idx),
geometry: { type: "LineString", coordinates: [s.from, s.to] }, geometry: { type: "LineString", coordinates: [s.from, s.to] },
properties: { properties: {
type: "fc", type: "fc",
@ -2702,12 +2729,18 @@ export function Map3D({
"line-width": ["case", ["==", ["get", "highlighted"], 1], 2.0, 1.3] as never, "line-width": ["case", ["==", ["get", "highlighted"], 1], 2.0, 1.3] as never,
"line-opacity": 0.9, "line-opacity": 0.9,
}, },
} as unknown as LayerSpecification, } as unknown as LayerSpecification,
before, before,
); );
} catch (e) { } catch (e) {
console.warn("FC lines layer add failed:", e); console.warn("FC lines layer add failed:", e);
} }
} else {
try {
map.setLayoutProperty(layerId, "visibility", "visible");
} catch {
// ignore
}
} }
reorderGlobeFeatureLayers(); reorderGlobeFeatureLayers();
@ -2718,7 +2751,6 @@ export function Map3D({
ensure(); ensure();
return () => { return () => {
stop(); stop();
remove();
}; };
}, [ }, [
projection, projection,
@ -2741,22 +2773,12 @@ export function Map3D({
const remove = () => { const remove = () => {
try { try {
if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId); if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, "visibility", "none");
} catch { } catch {
// ignore // ignore
} }
try { try {
if (map.getLayer(layerId)) map.removeLayer(layerId); if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
} catch {
// ignore
}
try {
if (map.getSource(srcId)) map.removeSource(srcId);
} catch {
// ignore
}
try {
if (map.getSource(fillSrcId)) map.removeSource(fillSrcId);
} catch { } catch {
// ignore // ignore
} }
@ -2772,11 +2794,11 @@ export function Map3D({
const fcLine: GeoJSON.FeatureCollection<GeoJSON.LineString> = { const fcLine: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: "FeatureCollection", type: "FeatureCollection",
features: (fleetCircles || []).map((c, idx) => { features: (fleetCircles || []).map((c) => {
const ring = circleRingLngLat(c.center, c.radiusNm * 1852); const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
return { return {
type: "Feature", type: "Feature",
id: `fleet-${c.ownerKey}-${idx}`, id: makeFleetCircleFeatureId(c.ownerKey),
geometry: { type: "LineString", coordinates: ring }, geometry: { type: "LineString", coordinates: ring },
properties: { properties: {
type: "fleet", type: "fleet",
@ -2795,11 +2817,11 @@ export function Map3D({
const fcFill: GeoJSON.FeatureCollection<GeoJSON.Polygon> = { const fcFill: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
type: "FeatureCollection", type: "FeatureCollection",
features: (fleetCircles || []).map((c, idx) => { features: (fleetCircles || []).map((c) => {
const ring = circleRingLngLat(c.center, c.radiusNm * 1852); const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
return { return {
type: "Feature", type: "Feature",
id: `fleet-fill-${c.ownerKey}-${idx}`, id: `${makeFleetCircleFeatureId(c.ownerKey)}-fill`,
geometry: { type: "Polygon", coordinates: [ring] }, geometry: { type: "Polygon", coordinates: [ring] },
properties: { properties: {
type: "fleet-fill", type: "fleet-fill",
@ -2859,6 +2881,12 @@ export function Map3D({
} catch (e) { } catch (e) {
console.warn("Fleet circles fill layer add failed:", e); console.warn("Fleet circles fill layer add failed:", e);
} }
} else {
try {
map.setLayoutProperty(fillLayerId, "visibility", "visible");
} catch {
// ignore
}
} }
if (!map.getLayer(layerId)) { if (!map.getLayer(layerId)) {
@ -2880,6 +2908,12 @@ export function Map3D({
} catch (e) { } catch (e) {
console.warn("Fleet circles layer add failed:", e); console.warn("Fleet circles layer add failed:", e);
} }
} else {
try {
map.setLayoutProperty(layerId, "visibility", "visible");
} catch {
// ignore
}
} }
reorderGlobeFeatureLayers(); reorderGlobeFeatureLayers();
@ -2890,7 +2924,6 @@ export function Map3D({
ensure(); ensure();
return () => { return () => {
stop(); stop();
remove();
}; };
}, [ }, [
projection, projection,
@ -2914,12 +2947,7 @@ export function Map3D({
const remove = () => { const remove = () => {
try { try {
if (map.getLayer(layerId)) map.removeLayer(layerId); if (map.getLayer(layerId)) map.setLayoutProperty(layerId, "visibility", "none");
} catch {
// ignore
}
try {
if (map.getSource(srcId)) map.removeSource(srcId);
} catch { } catch {
// ignore // ignore
} }
@ -2952,11 +2980,11 @@ export function Map3D({
const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = { const fc: GeoJSON.FeatureCollection<GeoJSON.LineString> = {
type: "FeatureCollection", type: "FeatureCollection",
features: ranges.map((c, idx) => { features: ranges.map((c) => {
const ring = circleRingLngLat(c.center, c.radiusNm * 1852); const ring = circleRingLngLat(c.center, c.radiusNm * 1852);
return { return {
type: "Feature", type: "Feature",
id: `pair-range-${idx}`, id: makePairLinkFeatureId(c.aMmsi, c.bMmsi),
geometry: { type: "LineString", coordinates: ring }, geometry: { type: "LineString", coordinates: ring },
properties: { properties: {
type: "pair-range", type: "pair-range",
@ -3007,6 +3035,12 @@ export function Map3D({
} catch (e) { } catch (e) {
console.warn("Pair range layer add failed:", e); console.warn("Pair range layer add failed:", e);
} }
} else {
try {
map.setLayoutProperty(layerId, "visibility", "visible");
} catch {
// ignore
}
} }
kickRepaint(map); kickRepaint(map);
@ -3016,7 +3050,6 @@ export function Map3D({
ensure(); ensure();
return () => { return () => {
stop(); stop();
remove();
}; };
}, [ }, [
projection, projection,