/* ============================================================================= * B05_Profile_UI_RouteEdit_Contour.ts * 계획노선 편집 모달이 바탕에 깔 **등고선 한 벌**을 고른다. * * **어느 등고선을 쓰나**(2026-09-12 사용자 지시 ⑥) — 도엽 등고선과 LAS 로 만든 등고선은 * 서로 어긋난다. 노선은 실제 지형 위에 놓여야 하므로 **확정 지표면 모델이 있으면 LAS 쪽**을 * 쓰고, 없는 프로젝트에서만 지금까지처럼 도엽 등고선을 쓴다. * * 둘은 생김새가 다르다 — 도엽은 위경도 GeoJSON(표고는 `등고수치` 속성), LAS 는 사업지 * 좌표(m) 점렬(표고는 `level`)이다. 여기서 **같은 `PreparedLayer` 한 꼴로 맞춰** 내보내 * 그리기·라벨·집기가 출처를 안 가리게 한다. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { fetchCachedJson } from "../A00_Common/b_asset_cache"; import { type GeoJsonCollection, type Normalizer, type PreparedLayer, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { prepareLayer, prepareMetricPolylines, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender_Prepare"; import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; /** 도엽 등고선의 표고 속성 이름 — B04 지도가 쓰는 것과 같은 키. */ const SHEET_ELEVATION_KEYS = ["등고수치"]; /** 표고를 못 읽었을 때 라벨 솎기에 쓸 간격(m). */ const FALLBACK_INTERVAL_M = 5; /** LAS 등고선을 **몇 m 단위로** 낼지 — 도엽 등고선과 같은 눈금(2026-09-12 사용자 지시 ⑭). * * LAS 자료는 1m 간격으로 뽑혀 있어 그대로 쓰면 확대할 때 1m·2m 짜리까지 나온다. 서버에 5m * 로 다시 뽑아 달라고 하면 첫 한 번이 오래 걸리므로 **받아 둔 1m 자료에서 5의 배수만 고른다** * — 그림도 라벨도 도엽 쪽과 같은 눈금이 된다. */ const LAS_CONTOUR_UNIT_M = 5; export interface RouteEditContours { layer: PreparedLayer; /** 등고선 간격(m) — 라벨을 몇 줄마다 낼지 정하는 기준. */ intervalM: number; source: "las" | "sheet"; } interface ContourResponse { contours: Array<{ level: number; coordinates: Array<[number, number, number]> }>; } /** * 바탕 등고선을 읽는다. 확정 지표면 모델이 있으면 LAS, 없으면 이미 받아 둔 도엽 컬렉션. * * LAS 쪽을 못 읽으면 **조용히 도엽으로 내려앉는다** — 등고선이 아예 없는 화면보다 낫고, * 어느 쪽을 쓰고 있는지는 `source` 로 나가 상태줄에 적힌다. */ export async function loadRouteEditContours( projectId: string, meta: VWorldMeta, normalizer: Normalizer, sheet: GeoJsonCollection | null, options: { surfaceModelId: number | null; intervalM: number; smooth: boolean }, ): Promise { if (options.surfaceModelId !== null) { // 받아 오는 간격은 프로젝트 설정 그대로(보관함에 이미 있는 파일을 쓰려는 것) — // **보이는 눈금**은 아래에서 5m 로 맞춘다. const interval = options.intervalM > 0 ? options.intervalM : 1; try { // 3D 뷰어가 쓰는 것과 **같은 파일**이다 — 보관함에 있으면 다시 내려받지 않는다. const data = await fetchCachedJson( projectId, `${API_BASE_URL}/projects/${projectId}/surface/models/${options.surfaceModelId}` + `/contour?interval=${interval}&smooth=${options.smooth}`, ); const lines = (data.contours ?? []) // 5m 단위만 남긴다 — 1m 자료를 다 들고 있으면 그리기·집기가 다섯 배로 무겁다. .filter((contour) => Math.abs(contour.level % LAS_CONTOUR_UNIT_M) < 1e-6) .map((contour) => ({ points: contour.coordinates.map(([x, y]) => [x, y] as const), label: contour.level, })) .filter((line) => line.points.length >= 2); if (lines.length > 0) { return { layer: prepareMetricPolylines(lines, meta), intervalM: LAS_CONTOUR_UNIT_M, source: "las", }; } } catch { /* 내려앉는다 — 아래 도엽 갈래로 이어 간다. */ } } const layer = prepareLayer(sheet ?? undefined, normalizer, SHEET_ELEVATION_KEYS); return { layer, intervalM: inferIntervalM(layer), source: "sheet" }; } /** 도엽 등고선의 간격(m) — 표고 값들의 **가장 좁은 칸**을 간격으로 본다. */ function inferIntervalM(layer: PreparedLayer): number { const levels = [ ...new Set( layer.features .map((feature) => feature.labelValue) .filter((value): value is number => value !== null), ), ].sort((a, b) => a - b); let smallest = Infinity; for (let index = 1; index < levels.length; index += 1) { const gap = levels[index] - levels[index - 1]; if (gap > 0 && gap < smallest) smallest = gap; } return Number.isFinite(smallest) ? smallest : FALLBACK_INTERVAL_M; }