Files
Aislo/B05_Profile/B05_Profile_Api_TerrainGrid.ts
T
eomsangdonandClaude Opus 5.5 4eec3a4b37 feat(509-5 · 509-2 B05 추천 지형): 분석 지형 고르기 = 추천 노선 창 안 · 지형마다 추천 미리 셈 · [적용] = 창 지형이 설계 지형 · 평면은 지형 이름만 · 설계 지형 격자 하나(수치지형도 적용이면 그 격자)
- 전처리 미리 셈이 고를 수 있는 지형마다(설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만) 다섯 갈래 + 초기 종단
- 창 지형 바꿈 = 그 지형 저장본만 · 없으면 409 안내(+[재계산]) · 기본값 판정에서 지형 뺌
- POST /route/initial terrain = 창 지형 · [적용] draft.terrain = 창 지형 · 손 [저장]은 지형 안 바꿈(서버도 as_initial 일 때만)
- 평면 분석 지형 고르기 · 화면 취향(routeedit-terrain) 걷음 · 지반고 · 묶음 초안 = 설계 지형(null) · 적용 뒤 받아 둔 격자 버림
- 시험 test_509_5 · test_509_2 · 걷은 고르기를 보던 옛 시험 새 동작으로(352_3 건너뜀)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:41:20 +09:00

112 lines
4.9 KiB
TypeScript

/* =============================================================================
* 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 { calcOnServer } from "../A00_Common/design/design_calc_sites";
import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_sampler";
import type { GroundStageDeps } from "./logic/B05_Profile_Pipeline_RouteGround";
export interface DesignTerrainGrid {
/** 이 격자의 지형 — 계산 묶음 노선 초안 지형(null = 설계)이 이것과 같을 때만 씀. */
design: "las" | "sheet";
grid: DtmGrid;
}
interface TerrainSourcesPayload {
design?: "las" | "sheet";
grid?: { model_id: number; smooth: boolean; signature: string } | null;
}
const loaded = new Map<string, Promise<DesignTerrainGrid | null>>();
/** 받기가 끝난 격자 — 끝나기 전엔 칸이 없음(그동안 서버 길). */
const ready = new Map<string, DesignTerrainGrid | null>();
async function download(projectId: string): Promise<DesignTerrainGrid | null> {
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<DesignTerrainGrid | null> {
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;
}
/** 설계 지형이 바뀜(추천 [적용] · 509-5) — 받아 둔 격자를 버림 · 다음 부름에 새 지형 격자를 받음. */
export function forgetDesignTerrainGrid(projectId: string): void {
loaded.delete(projectId);
ready.delete(projectId);
}
/** 지금 쓸 수 있는 격자 — 받는 중이면 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<GroundStageDeps, "grid" | "remote" | "serverOnly"> {
return {
grid: () => designTerrainGridNow(projectId),
serverOnly: () => calcOnServer("ground_sample"),
async remote(points, terrain, signal) {
const chunks: Array<Array<[number, number]>> = [];
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<number | null> }).z;
}),
);
return answers.flat();
},
};
}