Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_LidarUpload.ts
T
eomsangdonandClaude Opus 5.5 b3dcb73784 feat(637-3): B05 지형 칩 옆 [LAS 올리기] · 진행 서클 · 확인창 → 설계 지형 바꿈 · 공용 올리기 부품 map_lidar_upload(예정노선 같은 단추 · 흐름) · 갈음 때 안 지은 라이다 필터 파일 · 행 지움(요구 546)
- 공용 A00_Common/map/map_lidar_upload — 파일 고름 · POST multipart · 1 초 묻기 · 서클 · 끝난 뒤 일(주소 · 진행 묻기만 인자)
- 예정노선 UI_LidarUpload = 부품 부르기(화면 · 동작 그대로) · 안 쓰는 uploadPlanLidar 걷음
- B05 UI_RouteEdit_Terrain — 칩 · 단추 · 서클 한 칸 · 끝나면 확인창 「설계 지형을 라이다로 바꿈?」(이미 라이다 = force 다시 셈) · covered 거짓 = 바꿈 막고 알림 · 들어올 때 도는 일 이어 봄
- common_util_lidar_upload.drop_stale_filters — 갈음 뒤 안 지은 라이다 필터(옛 LAS) 모델 · 등고선 · 마스크 · 지면점 · manifest 줄 지움 · save_models drop = 그 행만(확정 행 남김)
- 시험 test_637_lidar(필터 지움 · 행 지움 · B05 화면) · test_624_4b(올리기 = 공용 부품으로 옮김)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:53:48 +09:00

143 lines
5.8 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_UI_LidarUpload.ts
* 예정노선 분석 지형(PLAN 624-4 화면 · 요구 530 · 531) — 도구줄 끝 칸: 공용 지형 칩(수치지형도 · 라이다) +
* 라이다 지형이 없으면 「라이다」 대신 [LAS 올리기](LAS · LAZ + .prj → 서버가 뒤에서 지형 · 진행 = 위치 칸 서클 하나 「라이다」) ·
* 끝나면 「라이다」를 고를 수 있음(저절로 안 바꿈).
* 고른 원천 = 화면 캐시(세션 `d03-terrain`)만 · 기본 = 수치지형도 · 라이다 지형이 없어지면 수치지형도로.
* 바꾸면 높이 격자(긋기 기울기)를 그 원천으로 받은 뒤 `onChange` — 겹(급경사 · 음영 · 0차곡)은 페이지가 다시 받음.
* 칩 안은 공용 그대로(`map_terrain_chip` · `onChange` · `setHave` 만 씀) · 올리기 = 공용 `map_lidar_upload`(637-3).
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
import { readState, writeState } from "../A00_Common/b_page_state";
import { createLidarUpload } from "../A00_Common/map/map_lidar_upload";
import { createTerrainChip, type TerrainSource } from "../A00_Common/map/map_terrain_chip";
import {
fetchPlanLidar,
fetchPlanProgress,
fetchPlanSurface,
type PlanSurface,
} from "./D03_PlanAnalysis_Api";
import { errorText, type WorkCircle } from "./D03_PlanAnalysis_UI_Terrain";
export interface PlanTerrain {
/** 도구줄 `terrain` 칸 — 칩 + [LAS 올리기]. */
root: HTMLElement;
/** 고른 원천(겹 받기 `terrain=`). */
source: () => TerrainSource;
/** 높이 격자 — 수치지형도 = 넘긴 사업 지형면 · 라이다 = 라이다 격자(범위 밖 = 빈칸). */
surface: (sheet: PlanSurface | null) => PlanSurface | null;
/** 올리기 단추 보임(편집 권한). */
setEditable: (on: boolean) => void;
/** 들어올 때 — 라이다 지형 있나 · 캐시 원천 되살림 · 도는 올리기 지켜봄(겹 받기 전에). */
load: () => Promise<void>;
/** 검증 창구. */
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 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<HTMLElement>('[data-value="lidar"]')!;
const lidarUpload = createLidarUpload({
path: `/projects/${projectId}/plan/lidar`,
poll: async () => (await fetchPlanProgress(projectId)).lidar,
show: (state) => work.terrain(state && { ...state, label: "라이다" }),
track: (job) => work.track(job),
onBusy: () => render(),
onDone: async () => {
try {
await readLidar();
if (ready) showToast("라이다 지형 끝 — 지형 칩에서 「라이다」를 고를 수 있음", "success");
} catch (error) {
showToast(`라이다 진행을 읽지 못했습니다 — ${errorText(error)}`, "error");
}
render();
},
});
const upload = lidarUpload.button;
// 마우스 글 = 지형 칸 표 호버제목(634-2 · 「수치지형도/라이다」 · 단추 글 「LAS 올리기」 그대로)
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;
}
async function readLidar(): Promise<void> {
const info = await fetchPlanLidar(projectId);
if (info.model_file !== modelFile) lidar = null; // 다시 올림 = 새 격자
[ready, modelFile] = [info.ready, info.model_file ?? ""];
}
/** 라이다 높이 격자 — 한 번 받아 둠. */
async function lidarSurface(): Promise<PlanSurface | null> {
lidar ??= await fetchPlanSurface(projectId, modelFile);
return lidar;
}
async function choose(next: TerrainSource): Promise<void> {
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 load(): Promise<void> {
try {
await readLidar();
if (ready && readState<TerrainSource>("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 lidarUpload.follow();
}
render();
return {
root,
source: () => source,
surface: (sheet) => (source === "lidar" ? lidar : sheet),
setEditable(on) {
editable = on;
render();
},
load,
state: () => ({ source, ready, uploading: lidarUpload.busy(), heights: !!lidar }),
};
}