/* ============================================================================= * D03_PlanAnalysis_UI_LidarUpload.ts * 예정노선 분석 지형(PLAN 624-4 화면 · 요구 530 · 531) — 도구줄 끝 칸: 공용 지형 칩(수치지형도 · 라이다) + * 라이다 지형이 없으면 「라이다」 대신 [LAS 올리기](LAS · LAZ + .prj → 서버가 뒤에서 지형 · 진행 = 위치 칸 서클 하나 「라이다」) · * 끝나면 「라이다」를 고를 수 있음(저절로 안 바꿈). * 고른 원천 = 화면 캐시(세션 `d03-terrain`)만 · 기본 = 수치지형도 · 라이다 지형이 없어지면 수치지형도로. * 바꾸면 높이 격자(긋기 기울기)를 그 원천으로 받은 뒤 `onChange` — 겹(급경사 · 음영 · 0차곡)은 페이지가 다시 받음. * 칩 안은 공용 그대로(`map_terrain_chip` · `onChange` · `setHave` 만 씀). * ========================================================================== */ import { createButton } from "@ui/ui_template_button"; import { pickFiles } from "@ui/ui_template_drop"; import { showToast } from "@ui/ui_template_elements"; import { readState, writeState } from "../A00_Common/b_page_state"; import { createTerrainChip, type TerrainSource } from "../A00_Common/map/map_terrain_chip"; import { fetchPlanLidar, fetchPlanProgress, fetchPlanSurface, uploadPlanLidar, type PlanSurface, } from "./D03_PlanAnalysis_Api"; import { errorText, sleep, type WorkCircle } from "./D03_PlanAnalysis_UI_Terrain"; const POLL_MS = 1000; export interface PlanTerrain { /** 도구줄 `terrain` 칸 — 칩 + [LAS 올리기]. */ root: HTMLElement; /** 고른 원천(겹 받기 `terrain=`). */ source: () => TerrainSource; /** 높이 격자 — 수치지형도 = 넘긴 사업 지형면 · 라이다 = 라이다 격자(범위 밖 = 빈칸). */ surface: (sheet: PlanSurface | null) => PlanSurface | null; /** 올리기 단추 보임(편집 권한). */ setEditable: (on: boolean) => void; /** 들어올 때 — 라이다 지형 있나 · 캐시 원천 되살림 · 도는 올리기 지켜봄(겹 받기 전에). */ load: () => Promise; /** 검증 창구. */ state: () => { source: TerrainSource; ready: boolean; uploading: boolean; heights: boolean }; } export function createPlanTerrain(opts: { projectId: string; /** 위치 칸 진행 서클(604-3 · 하나). */ work: WorkCircle; /** 원천이 바뀌고 높이 격자를 받은 뒤. */ onChange: (source: TerrainSource) => void; }): PlanTerrain { const { projectId, work } = opts; let source: TerrainSource = "sheet"; let ready = false; let uploading = false; let editable = false; let modelFile = ""; let lidar: PlanSurface | null = null; const chip = createTerrainChip({ page: "d03", value: source, have: ["sheet"], onChange: (next) => void choose(next), }); const lidarItem = chip.root.querySelector('[data-value="lidar"]')!; const upload = createButton({ label: "LAS 올리기", variant: "ghost", size: 32, iconStart: "upload", onClick: () => pickFiles({ accept: ".las,.laz,.prj", multiple: true }, (files) => void send(files)), }); upload.title = "라이다 LAS · LAZ(+ 좌표계 .prj 하나) — 올리면 서버가 지형을 지음 · 끝나면 「라이다」"; const root = document.createElement("div"); root.className = "d03-plan__terrain"; root.append(chip.root, upload); function render(): void { chip.setHave(ready ? ["sheet", "lidar"] : ["sheet"]); chip.set(source); lidarItem.style.display = !ready && editable ? "none" : ""; // 올릴 수 있으면 막힌 「라이다」 대신 단추 upload.hidden = ready || !editable; upload.disabled = uploading; } async function readLidar(): Promise { const info = await fetchPlanLidar(projectId); if (info.model_file !== modelFile) lidar = null; // 다시 올림 = 새 격자 [ready, modelFile] = [info.ready, info.model_file ?? ""]; } /** 라이다 높이 격자 — 한 번 받아 둠. */ async function lidarSurface(): Promise { lidar ??= await fetchPlanSurface(projectId, modelFile); return lidar; } async function choose(next: TerrainSource): Promise { if (next === source) return; try { if (next === "lidar" && !(await lidarSurface())) throw new Error("라이다 높이 격자가 없습니다"); } catch (error) { showToast(`라이다 지형을 읽지 못했습니다 — ${errorText(error)}`, "error"); return render(); } source = next; writeState("d03-terrain", source, projectId); render(); opts.onChange(source); } async function send(files: File[]): Promise { try { await uploadPlanLidar(projectId, files); } catch (error) { return showToast(`LAS 올리지 못함 — ${errorText(error)}`, "error"); } await work.track(follow()); } /** 올린 뒤 지형 짓기를 끝날 때까지 지켜봄 — 진행 = 서클. */ async function follow(): Promise { uploading = true; work.terrain({ ratio: 0, text: "", label: "라이다" }); render(); try { for (;;) { if (!root.isConnected) return; // 페이지 떠남 const job = (await fetchPlanProgress(projectId)).lidar; work.terrain({ ratio: job.percent / 100, text: job.message ?? "", label: "라이다" }); if (!job.running) { if (job.error) showToast(`라이다 지형 못 지음 — ${job.error}`, "error"); break; } await sleep(POLL_MS); } await readLidar(); if (ready) showToast("라이다 지형 끝 — 지형 칩에서 「라이다」를 고를 수 있음", "success"); } catch (error) { showToast(`라이다 진행을 읽지 못했습니다 — ${errorText(error)}`, "error"); } finally { uploading = false; work.terrain(null); render(); } } async function load(): Promise { try { await readLidar(); if (ready && readState("d03-terrain", projectId) === "lidar") { if (await lidarSurface()) source = "lidar"; } if (source === "sheet") writeState("d03-terrain", null, projectId); } catch (error) { showToast(`라이다 지형을 읽지 못했습니다 — ${errorText(error)}`, "error"); } render(); const job = await fetchPlanProgress(projectId).catch(() => null); if (job?.lidar.running) void work.track(follow()); } render(); return { root, source: () => source, surface: (sheet) => (source === "lidar" ? lidar : sheet), setEditable(on) { editable = on; render(); }, load, state: () => ({ source, ready, uploading, heights: !!lidar }), }; }