import maplibregl, { type GeoJSONSourceSpecification, type LayerSpecification, } from 'maplibre-gl'; export function removeLayerIfExists(map: maplibregl.Map, layerId: string | null | undefined) { if (!layerId) return; try { if (map.getLayer(layerId)) { map.removeLayer(layerId); } } catch { // ignore } } export function removeSourceIfExists(map: maplibregl.Map, sourceId: string) { try { if (map.getSource(sourceId)) { map.removeSource(sourceId); } } catch { // ignore } } // Ship 레이어/소스는 useGlobeShips에서 visibility 토글로 관리 (재생성 비용 회피) const GLOBE_NATIVE_LAYER_IDS = [ 'pair-lines-ml', 'fc-lines-ml', 'fleet-circles-ml', 'pair-range-ml', 'subcables-hitarea', 'subcables-casing', 'subcables-line', 'subcables-glow', 'subcables-points', 'subcables-label', 'deck-globe', ]; const GLOBE_NATIVE_SOURCE_IDS = [ 'pair-lines-ml-src', 'fc-lines-ml-src', 'fleet-circles-ml-src', 'pair-range-ml-src', 'subcables-src', 'subcables-pts-src', ]; export function clearGlobeNativeLayers(map: maplibregl.Map) { for (const id of GLOBE_NATIVE_LAYER_IDS) { removeLayerIfExists(map, id); } for (const id of GLOBE_NATIVE_SOURCE_IDS) { removeSourceIfExists(map, id); } } export function ensureGeoJsonSource( map: maplibregl.Map, sourceId: string, data: GeoJSON.GeoJSON, options?: Partial>, ) { const existing = map.getSource(sourceId); if (existing) { (existing as maplibregl.GeoJSONSource).setData(data); } else { map.addSource(sourceId, { type: 'geojson', data, ...options, } satisfies GeoJSONSourceSpecification); } } export function ensureLayer( map: maplibregl.Map, spec: LayerSpecification, options?: { before?: string }, ) { if (map.getLayer(spec.id)) return; const before = options?.before && map.getLayer(options.before) ? options.before : undefined; map.addLayer(spec, before); } export function setLayerVisibility(map: maplibregl.Map, layerId: string, visible: boolean) { if (!map.getLayer(layerId)) return; try { map.setLayoutProperty(layerId, 'visibility', visible ? 'visible' : 'none'); } catch { // ignore } } /** * setLayoutProperty('visibility') wrapper — 현재 값과 동일하면 호출 생략. * MapLibre는 setLayoutProperty 호출 시 항상 style._changed = true를 설정하여 * 모든 symbol layer의 placement를 재계산시킴. text-allow-overlap:false 라벨이 * 충돌 검사에 의해 사라지는 문제를 방지하기 위해, 값이 실제로 바뀔 때만 호출. */ export function guardedSetVisibility(map: maplibregl.Map, layerId: string, target: 'visible' | 'none') { if (!map.getLayer(layerId)) return; try { if (map.getLayoutProperty(layerId, 'visibility') === target) return; map.setLayoutProperty(layerId, 'visibility', target); } catch { // ignore } } export function cleanupLayers( map: maplibregl.Map, layerIds: string[], sourceIds: string[], ) { requestAnimationFrame(() => { for (const id of layerIds) { try { if (map.getLayer(id)) map.removeLayer(id); } catch { // ignore } } for (const id of sourceIds) { try { if (map.getSource(id)) map.removeSource(id); } catch { // ignore } } }); }