gc-wing/apps/web/src/widgets/map3d/hooks/useNativeMapLayers.ts

170 lines
5.9 KiB
TypeScript
Raw Normal View 히스토리

/**
* useNativeMapLayers Mercator/Globe MapLibre hook
*
* :
* - projectionBusy / isStyleLoaded
* - GeoJSON source /
* - Layer (ensureLayer)
* - Visibility
* - Globe (reorderGlobeFeatureLayers)
* - kickRepaint
* - Unmount cleanupLayers
*
* ,
* useEffect에서 .
*/
import { useEffect, useRef, type MutableRefObject } from 'react';
import maplibregl, { type GeoJSONSourceSpecification, type LayerSpecification } from 'maplibre-gl';
import { ensureGeoJsonSource, ensureLayer, setLayerVisibility, cleanupLayers } from '../lib/layerHelpers';
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
/* ── Public types ──────────────────────────────────────────────────── */
export interface NativeSourceConfig {
id: string;
data: GeoJSON.GeoJSON | null;
/** GeoJSON source 옵션 (tolerance, buffer 등) */
options?: Partial<Omit<GeoJSONSourceSpecification, 'type' | 'data'>>;
}
export interface NativeLayerSpec {
id: string;
type: 'line' | 'fill' | 'circle' | 'symbol';
sourceId: string;
paint: Record<string, unknown>;
layout?: Record<string, unknown>;
filter?: unknown[];
minzoom?: number;
maxzoom?: number;
}
export interface NativeMapLayersConfig {
/** GeoJSON 데이터 소스 (다중 지원) */
sources: NativeSourceConfig[];
/** 레이어 스펙 배열 (생성 순서대로) */
layers: NativeLayerSpec[];
/** 전체 레이어 on/off */
visible: boolean;
/**
* ID.
* .
*/
beforeLayer?: string | string[];
/**
* () .
* .
*/
onAfterSetup?: (map: maplibregl.Map) => void;
}
/* ── Hook ──────────────────────────────────────────────────────────── */
/**
* @param mapRef - Map ref
* @param projectionBusyRef - ref
* @param reorderGlobeFeatureLayers - Globe
* @param config - //visibility
* @param deps - .
* (subcableGeo, overlays.subcables, projection, mapSyncEpoch )
*/
export function useNativeMapLayers(
mapRef: MutableRefObject<maplibregl.Map | null>,
projectionBusyRef: MutableRefObject<boolean>,
reorderGlobeFeatureLayers: () => void,
config: NativeMapLayersConfig,
deps: readonly unknown[],
) {
// 최신 config를 항상 읽기 위한 ref (deps에 config 객체를 넣지 않기 위함)
const configRef = useRef(config);
useEffect(() => {
configRef.current = config;
});
/* ── 레이어 생성/데이터 업데이트 ─────────────────────────────────── */
useEffect(() => {
const map = mapRef.current;
if (!map) return;
const ensure = () => {
const cfg = configRef.current;
if (projectionBusyRef.current) return;
// 1. Visibility 토글
for (const spec of cfg.layers) {
setLayerVisibility(map, spec.id, cfg.visible);
}
// 2. 데이터가 있는 source가 하나도 없으면 종료
const hasData = cfg.sources.some((s) => s.data != null);
if (!hasData) return;
if (!map.isStyleLoaded()) return;
try {
// 3. Source 생성/업데이트
for (const src of cfg.sources) {
if (src.data) {
ensureGeoJsonSource(map, src.id, src.data, src.options);
}
}
// 4. Before layer 해석
let before: string | undefined;
if (cfg.beforeLayer) {
const candidates = Array.isArray(cfg.beforeLayer) ? cfg.beforeLayer : [cfg.beforeLayer];
for (const candidate of candidates) {
if (map.getLayer(candidate)) {
before = candidate;
break;
}
}
}
// 5. Layer 생성
const vis = cfg.visible ? 'visible' : 'none';
for (const spec of cfg.layers) {
const layerDef: Record<string, unknown> = {
id: spec.id,
type: spec.type,
source: spec.sourceId,
paint: spec.paint,
layout: { ...spec.layout, visibility: vis },
};
if (spec.filter) layerDef.filter = spec.filter;
if (spec.minzoom != null) layerDef.minzoom = spec.minzoom;
if (spec.maxzoom != null) layerDef.maxzoom = spec.maxzoom;
ensureLayer(map, layerDef as unknown as LayerSpecification, { before });
}
// 6. Post-setup callback
if (cfg.onAfterSetup) {
cfg.onAfterSetup(map);
}
} catch (e) {
console.warn('Native map layers setup failed:', e);
} finally {
reorderGlobeFeatureLayers();
kickRepaint(map);
}
};
const stop = onMapStyleReady(map, ensure);
return () => {
stop();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
/* ── Unmount cleanup ─────────────────────────────────────────────── */
useEffect(() => {
const mapInstance = mapRef.current;
return () => {
if (!mapInstance) return;
const cfg = configRef.current;
const layerIds = [...cfg.layers].reverse().map((l) => l.id);
const sourceIds = [...cfg.sources].reverse().map((s) => s.id);
cleanupLayers(mapInstance, layerIds, sourceIds);
};
}, []);
}