/* ============================================================================= * B05_Profile_UI_RouteEdit_Terrain.ts * 평면 설계 지형 칩(PLAN 626-1 · 요구 530 · 결정 A④ · D2) — 공용 지형 칩(`map_terrain_chip`)으로 설계 지형 보기 · 바꾸기. * 고를 수 있는 지형 = `/route/terrain-sources`(없는 쪽 막힘) · 바꾸면 확인창 → `POST /route/design-terrain` * (설계 지형 바꿈 · 뒤 단계 모두 다시 셈 · 노선 값 · 초기값 · 추천 저장본 그대로 · 잠금 막) → 초안 비움 → 페이지 다시 세움. * 취소 · 실패 = 칩을 지금 지형으로 되돌림. 옛 길(추천 [적용] 안 지형 고르기 · 509-5) 대신. * 칩 옆 [LAS 올리기](637-3 · 공용 `map_lidar_upload` · 예정노선과 같은 단추) → `POST /route/lidar` → 진행 서클(1 초 묻기) * → 끝나면 칩 다시 읽고 확인창 「설계 지형을 라이다로 바꿈?」(이미 라이다 = 갈음 → `force` 다시 셈) · * 노선을 다 못 덮음(`covered` 거짓) = 라이다로 바꿈 막고 알림 · 설계 값은 확인창을 눌러야만 바뀜. * 부르는 곳 = `_Chrome`(도구줄 `terrain` 칸) · `_RouteEdit`(지형 읽은 뒤 `load`). * ========================================================================== */ import { clearDrafts } from "../A00_Common/b_page_state"; import { createLidarUpload, type LidarJob } from "../A00_Common/map/map_lidar_upload"; import { createTerrainChip, TERRAIN_NAMES, type TerrainSource, } from "../A00_Common/map/map_terrain_chip"; import { askConfirm } from "@ui/ui_template_modal"; import { hideLoadingOverlay, showLoadingOverlay, showToast } from "@ui/ui_template_elements"; import { createProgressCircle } from "@ui/ui_template_progress"; import { requestJson, type AnalysisTerrain } from "./B05_Profile_Api_Replan"; import { forgetDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid"; import { rebuildAfterRouteSave } from "./B05_Profile_UI_RouteEdit_Apply"; /** 서버 설계 지형 이름(`las`) ↔ 공용 칩 값(`lidar`). */ const toChip = (terrain: AnalysisTerrain): TerrainSource => (terrain === "las" ? "lidar" : "sheet"); const toServer = (source: TerrainSource): AnalysisTerrain => (source === "lidar" ? "las" : "sheet"); /** `GET /route/lidar`(637-2) — 진행 + 올린 라이다 메타(`covered_ratio` 없음 = 올린 적 없음). */ interface RouteLidar extends LidarJob { ready: boolean; covered: boolean; covered_ratio: number | null; design: AnalysisTerrain; sources: AnalysisTerrain[]; } export interface RouteTerrain { root: HTMLElement; /** 지형을 읽음(못 읽음 = null · 칩 숨김). */ load: (sources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null) => void; /** 지금 설계 지형. */ design: () => AnalysisTerrain; } export function createRouteTerrain(projectId: string): RouteTerrain { let design: AnalysisTerrain = "las"; let busy = false; /** 올린 라이다 덮임 — 다 덮음 = null(올린 적 없음 포함 · 안 막음) · 못 덮음 = 덮인 비율. */ let uncovered: number | null = null; const chip = createTerrainChip({ page: "b05", value: toChip(design), have: [], onChange: (value) => void change(toServer(value)), }); const circle = createProgressCircle({ size: 16, inline: true }); circle.root.hidden = true; const lidarPath = `/projects/${projectId}/route/lidar`; const upload = createLidarUpload({ path: lidarPath, poll: () => requestJson(lidarPath, { method: "GET" }, 60000), show: (state) => { circle.root.hidden = !state; if (!state) return; circle.set(state.ratio, "라이다"); circle.root.title = `라이다 짓기 ${Math.round(state.ratio * 100)}% · ${state.text}`; }, onDone: afterUpload, }); const root = document.createElement("div"); root.className = "b05-routeedit__terrain"; root.append(chip.root, upload.button, circle.root); root.hidden = true; /** 올림 끝 — 칩 다시 · 덮임 보고 확인창(설계 지형 바꿈 · 이미 라이다 = 다시 셈). */ async function afterUpload(job: RouteLidar): Promise { if (job.interrupted) showToast("라이다 짓기가 끊겼습니다 — LAS 를 다시 올리세요.", "error"); if (job.error || job.interrupted || !job.ready) return; note(job); load({ sources: job.sources, design: job.design === "sheet" ? "sheet" : "las" }); if (uncovered != null) return warnUncovered(); await change("las", design === "las"); } const note = (info: RouteLidar): void => { uncovered = info.covered_ratio != null && !info.covered ? info.covered_ratio : null; }; const warnUncovered = (): void => showToast( `올린 라이다가 노선을 다 덮지 못합니다(덮임 ${Math.round((uncovered ?? 0) * 100)}%) — ` + "설계 지형을 라이다로 바꾸지 않습니다. 노선을 덮는 LAS 를 다시 올리세요.", "warning", 6000, ); /** 들어올 때 — 덮임 · 도는 올리기 이어 보기. */ async function resume(): Promise { const info = await requestJson(lidarPath, { method: "GET" }, 60000).catch( () => null, ); if (!info) return; note(info); if (info.running && !upload.busy()) void upload.follow(); } /** 설계 지형 바꿈 — `force` = 같은 지형 다시 셈(라이다 갈음 뒤). */ async function change(next: AnalysisTerrain, force = false): Promise { if (busy || upload.busy() || (next === design && !force)) return chip.set(toChip(design)); if (next === "las" && uncovered != null) { warnUncovered(); return chip.set(toChip(design)); } const ok = await askConfirm({ title: force ? "설계 지형 다시 셈" : next === "las" ? "설계 지형을 라이다로 바꿈?" : "설계 지형 바꿈", message: (force ? "새로 올린 라이다로 설계 지형을 다시 셈합니다 — 종단 · 횡단 · 배수 · 토공을 모두 다시 셈합니다. " : `설계 지형을 ${TERRAIN_NAMES[toChip(next)]}(으)로 바꿉니다 — 종단 · 횡단 · 배수 · 토공을 모두 다시 셈합니다. `) + "평면 노선 · 초기값([초기화])은 그대로 · 저장 안 한 고침은 버립니다." + (force ? " 안 하면 설계 값은 옛 라이다 셈 그대로입니다." : ""), okLabel: force ? "다시 셈" : "바꿈", }); if (!ok) return chip.set(toChip(design)); busy = true; showLoadingOverlay(); try { await requestJson( `/projects/${projectId}/route/design-terrain`, { method: "POST", body: JSON.stringify({ terrain: next, force }) }, 600000, ); design = next; clearDrafts(projectId); forgetDesignTerrainGrid(projectId); // 새 지형 격자로(509-2) showToast( `설계 지형을 ${TERRAIN_NAMES[toChip(next)]}(으)로 ${force ? "다시 셈했" : "바꿨"}습니다.`, "success", ); await rebuildAfterRouteSave(projectId); } catch (error) { chip.set(toChip(design)); showToast( `설계 지형을 바꾸지 못했습니다.${error instanceof Error ? ` ${error.message}` : ""}`, "error", ); } finally { busy = false; hideLoadingOverlay(); } } function load(sources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null): void { root.hidden = !sources; if (!sources) return; design = sources.design; chip.set(toChip(design)); chip.setHave(sources.sources.map(toChip)); } return { root, load(sources) { load(sources); if (sources) void resume(); }, design: () => design, }; }