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}`;
}
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<GeoJSON.LineString> = {
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<GeoJSON.LineString> = {
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<GeoJSON.Polygon> = {
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<GeoJSON.LineString> = {
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,