fix(map): 해저케이블 호버/프로젝션 버그
- useEffect 3개 분리 (레이어생성/호버/이벤트) - hoveredCableId를 레이어 생성 deps에서 분리하여 깜박임 제거 - 이벤트 바인딩에 retry 로직 추가 (프로젝션 전환 후) - paint 기본값을 상수로 추출하여 일관성 보장 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
7eff97afd4
커밋
fb1334ce45
@ -20,6 +20,14 @@ const LABEL_ID = 'subcables-label';
|
|||||||
const ALL_LAYER_IDS = [LABEL_ID, POINTS_ID, GLOW_ID, LINE_ID, CASING_ID, HITAREA_ID];
|
const ALL_LAYER_IDS = [LABEL_ID, POINTS_ID, GLOW_ID, LINE_ID, CASING_ID, HITAREA_ID];
|
||||||
const ALL_SOURCE_IDS = [POINTS_SRC_ID, SRC_ID];
|
const ALL_SOURCE_IDS = [POINTS_SRC_ID, SRC_ID];
|
||||||
|
|
||||||
|
/* ── Paint defaults (used for layer creation + hover reset) ──────── */
|
||||||
|
const LINE_OPACITY_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 0.7, 6, 0.82, 10, 0.92];
|
||||||
|
const LINE_WIDTH_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 1.6, 6, 2.5, 10, 4.0];
|
||||||
|
const CASING_OPACITY_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 0.3, 5, 0.5, 8, 0.65];
|
||||||
|
const CASING_WIDTH_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 2.4, 6, 3.6, 10, 5.5];
|
||||||
|
const POINTS_OPACITY_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 0.5, 5, 0.7, 8, 0.85];
|
||||||
|
const POINTS_RADIUS_DEFAULT = ['interpolate', ['linear'], ['zoom'], 2, 1.5, 6, 2.5, 10, 4];
|
||||||
|
|
||||||
export function useSubcablesLayer(
|
export function useSubcablesLayer(
|
||||||
mapRef: MutableRefObject<maplibregl.Map | null>,
|
mapRef: MutableRefObject<maplibregl.Map | null>,
|
||||||
projectionBusyRef: MutableRefObject<boolean>,
|
projectionBusyRef: MutableRefObject<boolean>,
|
||||||
@ -41,6 +49,9 @@ export function useSubcablesLayer(
|
|||||||
onHoverRef.current = onHoverCable;
|
onHoverRef.current = onHoverCable;
|
||||||
onClickRef.current = onClickCable;
|
onClickRef.current = onClickCable;
|
||||||
|
|
||||||
|
const hoveredCableIdRef = useRef(hoveredCableId);
|
||||||
|
hoveredCableIdRef.current = hoveredCableId;
|
||||||
|
|
||||||
/* ── Derived point features (cable midpoints for circle markers) ── */
|
/* ── Derived point features (cable midpoints for circle markers) ── */
|
||||||
const pointsGeoJson = useMemo<GeoJSON.FeatureCollection>(() => {
|
const pointsGeoJson = useMemo<GeoJSON.FeatureCollection>(() => {
|
||||||
if (!subcableGeo) return { type: 'FeatureCollection', features: [] };
|
if (!subcableGeo) return { type: 'FeatureCollection', features: [] };
|
||||||
@ -57,7 +68,11 @@ export function useSubcablesLayer(
|
|||||||
return { type: 'FeatureCollection', features };
|
return { type: 'FeatureCollection', features };
|
||||||
}, [subcableGeo]);
|
}, [subcableGeo]);
|
||||||
|
|
||||||
/* ── Main layer setup effect ──────────────────────────────────────── */
|
/* ================================================================
|
||||||
|
* Effect 1: Layer creation & data update
|
||||||
|
* - Does NOT depend on hoveredCableId (prevents flicker)
|
||||||
|
* - Creates sources, layers, sets visibility
|
||||||
|
* ================================================================ */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
@ -111,8 +126,8 @@ export function useSubcablesLayer(
|
|||||||
source: SRC_ID,
|
source: SRC_ID,
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': 'rgba(0,0,0,0.55)',
|
'line-color': 'rgba(0,0,0,0.55)',
|
||||||
'line-width': ['interpolate', ['linear'], ['zoom'], 2, 2.4, 6, 3.6, 10, 5.5],
|
'line-width': CASING_WIDTH_DEFAULT,
|
||||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 2, 0.3, 5, 0.5, 8, 0.65],
|
'line-opacity': CASING_OPACITY_DEFAULT,
|
||||||
},
|
},
|
||||||
layout: { visibility: vis, 'line-cap': 'round', 'line-join': 'round' },
|
layout: { visibility: vis, 'line-cap': 'round', 'line-join': 'round' },
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
@ -128,8 +143,8 @@ export function useSubcablesLayer(
|
|||||||
source: SRC_ID,
|
source: SRC_ID,
|
||||||
paint: {
|
paint: {
|
||||||
'line-color': ['get', 'color'],
|
'line-color': ['get', 'color'],
|
||||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 2, 0.7, 6, 0.82, 10, 0.92],
|
'line-opacity': LINE_OPACITY_DEFAULT,
|
||||||
'line-width': ['interpolate', ['linear'], ['zoom'], 2, 1.6, 6, 2.5, 10, 4.0],
|
'line-width': LINE_WIDTH_DEFAULT,
|
||||||
},
|
},
|
||||||
layout: { visibility: vis, 'line-cap': 'round', 'line-join': 'round' },
|
layout: { visibility: vis, 'line-cap': 'round', 'line-join': 'round' },
|
||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
@ -163,9 +178,9 @@ export function useSubcablesLayer(
|
|||||||
type: 'circle',
|
type: 'circle',
|
||||||
source: POINTS_SRC_ID,
|
source: POINTS_SRC_ID,
|
||||||
paint: {
|
paint: {
|
||||||
'circle-radius': ['interpolate', ['linear'], ['zoom'], 2, 1.5, 6, 2.5, 10, 4],
|
'circle-radius': POINTS_RADIUS_DEFAULT,
|
||||||
'circle-color': ['get', 'color'],
|
'circle-color': ['get', 'color'],
|
||||||
'circle-opacity': ['interpolate', ['linear'], ['zoom'], 2, 0.5, 5, 0.7, 8, 0.85],
|
'circle-opacity': POINTS_OPACITY_DEFAULT,
|
||||||
'circle-stroke-color': 'rgba(0,0,0,0.5)',
|
'circle-stroke-color': 'rgba(0,0,0,0.5)',
|
||||||
'circle-stroke-width': 0.5,
|
'circle-stroke-width': 0.5,
|
||||||
},
|
},
|
||||||
@ -202,67 +217,8 @@ export function useSubcablesLayer(
|
|||||||
} as unknown as LayerSpecification,
|
} as unknown as LayerSpecification,
|
||||||
);
|
);
|
||||||
|
|
||||||
/* ── Hover highlight (flat values — no nested interpolate) ── */
|
// Re-apply current hover state after layer (re-)creation
|
||||||
if (hoveredCableId) {
|
applyHoverHighlight(map, hoveredCableIdRef.current);
|
||||||
const matchExpr = ['==', ['get', 'id'], hoveredCableId];
|
|
||||||
|
|
||||||
// Main line: hovered=bright+thick, rest=dimmed+thin
|
|
||||||
if (map.getLayer(LINE_ID)) {
|
|
||||||
map.setPaintProperty(LINE_ID, 'line-opacity', ['case', matchExpr, 1.0, 0.2] as never);
|
|
||||||
map.setPaintProperty(LINE_ID, 'line-width', ['case', matchExpr, 4.5, 1.2] as never);
|
|
||||||
}
|
|
||||||
// Casing: dim non-hovered
|
|
||||||
if (map.getLayer(CASING_ID)) {
|
|
||||||
map.setPaintProperty(CASING_ID, 'line-opacity', ['case', matchExpr, 0.7, 0.12] as never);
|
|
||||||
map.setPaintProperty(CASING_ID, 'line-width', ['case', matchExpr, 6.5, 2.0] as never);
|
|
||||||
}
|
|
||||||
// Glow: show only on hovered cable
|
|
||||||
if (map.getLayer(GLOW_ID)) {
|
|
||||||
map.setFilter(GLOW_ID, matchExpr as never);
|
|
||||||
map.setPaintProperty(GLOW_ID, 'line-opacity', 0.35);
|
|
||||||
}
|
|
||||||
// Points: dim non-hovered
|
|
||||||
if (map.getLayer(POINTS_ID)) {
|
|
||||||
map.setPaintProperty(POINTS_ID, 'circle-opacity', ['case', matchExpr, 1.0, 0.15] as never);
|
|
||||||
map.setPaintProperty(POINTS_ID, 'circle-radius', ['case', matchExpr, 4, 1.5] as never);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Restore zoom-based interpolation defaults
|
|
||||||
if (map.getLayer(LINE_ID)) {
|
|
||||||
map.setPaintProperty(
|
|
||||||
LINE_ID, 'line-opacity',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 0.7, 6, 0.82, 10, 0.92] as never,
|
|
||||||
);
|
|
||||||
map.setPaintProperty(
|
|
||||||
LINE_ID, 'line-width',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 1.6, 6, 2.5, 10, 4.0] as never,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (map.getLayer(CASING_ID)) {
|
|
||||||
map.setPaintProperty(
|
|
||||||
CASING_ID, 'line-opacity',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 0.3, 5, 0.5, 8, 0.65] as never,
|
|
||||||
);
|
|
||||||
map.setPaintProperty(
|
|
||||||
CASING_ID, 'line-width',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 2.4, 6, 3.6, 10, 5.5] as never,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (map.getLayer(GLOW_ID)) {
|
|
||||||
map.setFilter(GLOW_ID, ['==', ['get', 'id'], ''] as never);
|
|
||||||
map.setPaintProperty(GLOW_ID, 'line-opacity', 0);
|
|
||||||
}
|
|
||||||
if (map.getLayer(POINTS_ID)) {
|
|
||||||
map.setPaintProperty(
|
|
||||||
POINTS_ID, 'circle-opacity',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 0.5, 5, 0.7, 8, 0.85] as never,
|
|
||||||
);
|
|
||||||
map.setPaintProperty(
|
|
||||||
POINTS_ID, 'circle-radius',
|
|
||||||
['interpolate', ['linear'], ['zoom'], 2, 1.5, 6, 2.5, 10, 4] as never,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Subcables layer setup failed:', e);
|
console.warn('Subcables layer setup failed:', e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -275,14 +231,35 @@ export function useSubcablesLayer(
|
|||||||
return () => {
|
return () => {
|
||||||
stop();
|
stop();
|
||||||
};
|
};
|
||||||
}, [subcableGeo, pointsGeoJson, overlays.subcables, projection, mapSyncEpoch, hoveredCableId, reorderGlobeFeatureLayers]);
|
// hoveredCableId intentionally excluded — handled by Effect 2
|
||||||
|
}, [subcableGeo, pointsGeoJson, overlays.subcables, projection, mapSyncEpoch, reorderGlobeFeatureLayers]);
|
||||||
|
|
||||||
/* ── Mouse events (bind to hit-area layer for easy hovering) ───── */
|
/* ================================================================
|
||||||
|
* Effect 2: Hover highlight (paint-only, no layer creation)
|
||||||
|
* - Lightweight, no flicker
|
||||||
|
* ================================================================ */
|
||||||
|
useEffect(() => {
|
||||||
|
const map = mapRef.current;
|
||||||
|
if (!map || !map.isStyleLoaded()) return;
|
||||||
|
if (projectionBusyRef.current) return;
|
||||||
|
if (!map.getLayer(LINE_ID)) return;
|
||||||
|
|
||||||
|
applyHoverHighlight(map, hoveredCableId);
|
||||||
|
kickRepaint(map);
|
||||||
|
}, [hoveredCableId]);
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Effect 3: Mouse events (bind to hit-area for easy hovering)
|
||||||
|
* - Retries binding until layer exists
|
||||||
|
* ================================================================ */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
if (!overlays.subcables) return;
|
if (!overlays.subcables) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
const onMouseMove = (e: maplibregl.MapMouseEvent & { features?: maplibregl.MapGeoJSONFeature[] }) => {
|
const onMouseMove = (e: maplibregl.MapMouseEvent & { features?: maplibregl.MapGeoJSONFeature[] }) => {
|
||||||
const cableId = e.features?.[0]?.properties?.id;
|
const cableId = e.features?.[0]?.properties?.id;
|
||||||
if (typeof cableId === 'string' && cableId) {
|
if (typeof cableId === 'string' && cableId) {
|
||||||
@ -303,22 +280,28 @@ export function useSubcablesLayer(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEvents = () => {
|
const bindEvents = () => {
|
||||||
// Bind to hit-area for wider hover target, fallback to main line
|
if (cancelled) return;
|
||||||
const targetLayer = map.getLayer(HITAREA_ID) ? HITAREA_ID : LINE_ID;
|
const targetLayer = map.getLayer(HITAREA_ID) ? HITAREA_ID : map.getLayer(LINE_ID) ? LINE_ID : null;
|
||||||
if (!map.getLayer(targetLayer)) return;
|
if (!targetLayer) {
|
||||||
|
// Layer not yet created — retry after short delay
|
||||||
|
retryTimer = setTimeout(bindEvents, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
map.on('mousemove', targetLayer, onMouseMove);
|
map.on('mousemove', targetLayer, onMouseMove);
|
||||||
map.on('mouseleave', targetLayer, onMouseLeave);
|
map.on('mouseleave', targetLayer, onMouseLeave);
|
||||||
map.on('click', targetLayer, onClick);
|
map.on('click', targetLayer, onClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (map.isStyleLoaded() && (map.getLayer(HITAREA_ID) || map.getLayer(LINE_ID))) {
|
if (map.isStyleLoaded()) {
|
||||||
addEvents();
|
bindEvents();
|
||||||
} else {
|
} else {
|
||||||
map.once('idle', addEvents);
|
map.once('idle', bindEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
if (retryTimer) clearTimeout(retryTimer);
|
||||||
try {
|
try {
|
||||||
map.off('mousemove', HITAREA_ID, onMouseMove);
|
map.off('mousemove', HITAREA_ID, onMouseMove);
|
||||||
map.off('mouseleave', HITAREA_ID, onMouseLeave);
|
map.off('mouseleave', HITAREA_ID, onMouseLeave);
|
||||||
@ -341,3 +324,44 @@ export function useSubcablesLayer(
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Hover highlight helper (paint-only mutations) ────────────────── */
|
||||||
|
function applyHoverHighlight(map: maplibregl.Map, hoveredId: string | null) {
|
||||||
|
if (hoveredId) {
|
||||||
|
const matchExpr = ['==', ['get', 'id'], hoveredId];
|
||||||
|
|
||||||
|
if (map.getLayer(LINE_ID)) {
|
||||||
|
map.setPaintProperty(LINE_ID, 'line-opacity', ['case', matchExpr, 1.0, 0.2] as never);
|
||||||
|
map.setPaintProperty(LINE_ID, 'line-width', ['case', matchExpr, 4.5, 1.2] as never);
|
||||||
|
}
|
||||||
|
if (map.getLayer(CASING_ID)) {
|
||||||
|
map.setPaintProperty(CASING_ID, 'line-opacity', ['case', matchExpr, 0.7, 0.12] as never);
|
||||||
|
map.setPaintProperty(CASING_ID, 'line-width', ['case', matchExpr, 6.5, 2.0] as never);
|
||||||
|
}
|
||||||
|
if (map.getLayer(GLOW_ID)) {
|
||||||
|
map.setFilter(GLOW_ID, matchExpr as never);
|
||||||
|
map.setPaintProperty(GLOW_ID, 'line-opacity', 0.35);
|
||||||
|
}
|
||||||
|
if (map.getLayer(POINTS_ID)) {
|
||||||
|
map.setPaintProperty(POINTS_ID, 'circle-opacity', ['case', matchExpr, 1.0, 0.15] as never);
|
||||||
|
map.setPaintProperty(POINTS_ID, 'circle-radius', ['case', matchExpr, 4, 1.5] as never);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (map.getLayer(LINE_ID)) {
|
||||||
|
map.setPaintProperty(LINE_ID, 'line-opacity', LINE_OPACITY_DEFAULT as never);
|
||||||
|
map.setPaintProperty(LINE_ID, 'line-width', LINE_WIDTH_DEFAULT as never);
|
||||||
|
}
|
||||||
|
if (map.getLayer(CASING_ID)) {
|
||||||
|
map.setPaintProperty(CASING_ID, 'line-opacity', CASING_OPACITY_DEFAULT as never);
|
||||||
|
map.setPaintProperty(CASING_ID, 'line-width', CASING_WIDTH_DEFAULT as never);
|
||||||
|
}
|
||||||
|
if (map.getLayer(GLOW_ID)) {
|
||||||
|
map.setFilter(GLOW_ID, ['==', ['get', 'id'], ''] as never);
|
||||||
|
map.setPaintProperty(GLOW_ID, 'line-opacity', 0);
|
||||||
|
}
|
||||||
|
if (map.getLayer(POINTS_ID)) {
|
||||||
|
map.setPaintProperty(POINTS_ID, 'circle-opacity', POINTS_OPACITY_DEFAULT as never);
|
||||||
|
map.setPaintProperty(POINTS_ID, 'circle-radius', POINTS_RADIUS_DEFAULT as never);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user