- GeoJSON source tolerance:1, buffer:64 (저줌 vertex 단순화) - hitarea/casing/glow 레이어 minzoom:3 (저줌 렌더 제외) - ensureGeoJsonSource에 source options 파라미터 추가 - NativeSourceConfig에 options 필드 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
5.9 KiB
TypeScript
170 lines
5.9 KiB
TypeScript
/**
|
|
* 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);
|
|
};
|
|
}, []);
|
|
}
|