/* ============================================================================= * B05_Profile_Api_TerrainGrid.ts * 설계 지형 격자를 받아 둔다 — 노선 편집이 지반고를 브라우저에서 바로 읽게(PLAN 344-1). * * GET /projects/{id}/route/terrain-sources → 설계 지형 · 격자 받을 자리(`grid`) * GET /projects/{id}/surface/models/{mid}/grid → DTM 격자 원시 이진(ETag · IndexedDB) * POST /projects/{id}/route/elevations → 격자를 못 쓸 때 계산 묶음 ground 단계의 서버 길 * * 주소에 manifest 서명을 붙임 — 보관함은 옛 저장본을 먼저 내므로(`fetchCachedBytes`) 지형이 * 바뀐 뒤 옛 격자로 한 번 그리는 일을 막음. DTM 아닌 표현 · 못 받음 = null → 서버 표본 길. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { fetchCachedBytes } from "../A00_Common/b_asset_cache"; import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler"; import type { GroundStageDeps } from "./logic/B05_Profile_Pipeline_RouteGround"; export interface DesignTerrainGrid { /** 이 격자의 지형 — 편집 창 분석 지형이 이것과 같을 때만 씀. */ design: "las" | "sheet"; grid: DtmGrid; } interface TerrainSourcesPayload { design?: "las" | "sheet"; grid?: { model_id: number; smooth: boolean; signature: string } | null; } const loaded = new Map>(); /** 받기가 끝난 격자 — 끝나기 전엔 칸이 없음(그동안 서버 길). */ const ready = new Map(); async function download(projectId: string): Promise { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/route/terrain-sources`, { credentials: "include", }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const payload = (await response.json()) as TerrainSourcesPayload; const info = payload.grid; if (!info) return null; const url = `${API_BASE_URL}/projects/${projectId}/surface/models/${info.model_id}/grid` + `?smooth=${info.smooth}&sig=${encodeURIComponent(info.signature)}`; const grid = parseDtmGrid(await fetchCachedBytes(projectId, url)); return { design: payload.design === "sheet" ? "sheet" : "las", grid }; } /** 설계 지형 격자 — 페이지 수명 동안 한 번만 받음 · 받다 실패하면 null(다음 부름에 다시 해 봄). */ export function loadDesignTerrainGrid(projectId: string): Promise { let pending = loaded.get(projectId); if (!pending) { pending = download(projectId).then( (value) => { ready.set(projectId, value); return value; }, () => { loaded.delete(projectId); return null; }, ); loaded.set(projectId, pending); } return pending; } /** 지금 쓸 수 있는 격자 — 받는 중이면 null 을 돌려주고 받기를 띄움(기다리지 않음). */ export function designTerrainGridNow(projectId: string): DesignTerrainGrid | null { if (!ready.has(projectId)) void loadDesignTerrainGrid(projectId); return ready.get(projectId) ?? null; } /** 한 번에 물을 수 있는 점 수 — 서버 `MAX_POINTS` 와 같은 값. */ const ELEVATION_CHUNK = 4000; /** 계산 묶음 ground 단계에 넣을 격자 · 서버 길(PLAN 346-1). 서버 길도 소수 6자리 — 격자 길과 같은 값. */ export function groundStageDeps(projectId: string): Pick { return { grid: () => designTerrainGridNow(projectId), async remote(points, terrain, signal) { const chunks: Array> = []; for (let i = 0; i < points.length; i += ELEVATION_CHUNK) { chunks.push(points.slice(i, i + ELEVATION_CHUNK)); } const answers = await Promise.all( chunks.map(async (chunk) => { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/route/elevations`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ points: chunk, terrain, decimals: 6 }), signal, }); if (!response.ok) throw new Error(`지반고 요청 실패: HTTP ${response.status}`); return ((await response.json()) as { z: Array }).z; }), ); return answers.flat(); }, }; }