Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_LidarUpload.ts
T
eomsangdonandClaude Opus 5.5 61733d064d feat(지도): 634-2 · 634-3 아이콘 표 두 장 · 묶음 메뉴 · 한 행 배선 · 호버 = 표(요구 546)
- 겹 표(map_layer_list) = 그룹1 22 줄 표 차례 · title = 호버제목 · group(수계망도 · 도로 · 토지·건물 · 제한·재해) · pages · 숨김 표 걷음 · 도로300m 한 줄(「도로」 묶음 · 기능 남김)
- 겹 줄(map_layer_bar) = 단독 넷 | 묶음 넷(여럿 고르기 메뉴 · 켠 수 배지 · 안 닫힘) | 그래픽 카드 · sync 가 단추 · 메뉴 줄 막힘 · 글을 맞춤(why = 페이지 까닭)
- 도구 표(map_toolbar) = 그룹2 17 줄 title · mapToolGroups 가 글을 표로 맞춤 · 기본 단추 생성 · 지형 칩 막힘 「수치지형도/라이다 — 까닭」
- 띠 = 한 자료 · 겹 둘(GL fillEdge 거름 · 옛 캔버스 · 범례 줄 따로) · B05 띠 받기(D03 과 같은 서버 길 · bandCollection 공용)
- D03 = 겹 줄 · 도구줄 한 행(createMapToolRow) · 222px 기댐 · 줄바꿈 CSS 걷음 · 측점 · 기울기 호버 덮어쓰기 걷음 · LAS 올리기 호버 걷음
- B05 = 세로 두 줄 → 한 행 · 호버 덮어쓰기 걷음(도구줄 · 1m 등고선 · 3D · 추천) · 겹 막힘 querySelector 덮어쓰기 → 공용 sync · 임도(업로드) 보임 + 「받은 임도 자료 없음」 막힘 · 스냅 간격 날 select → 고르기 메뉴(하나)
- 켬끔 기억 열쇠 · 측점 · 기울기 처음 값 · 그래픽 카드 취향 열쇠 둘 그대로
- 낡은 시험 고침(568_1 · 622_2 · 624_2 · 624_3 · 553_4 · 625_draw · b05_routeedit_initial · 626_3 · 390_4 · 346_2 · 353_1 · 581_2 · 584_1 · 584_3) — 표 꼴 row() · 호버 글 = 표 · 한 행 · 고르기 메뉴로 바뀐 글을 잡음 · 새 지킴 634_2

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

176 lines
6.7 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` 만 씀).
* ========================================================================== */
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<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 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<HTMLElement>('[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)),
});
// 마우스 글 = 지형 칸 표 호버제목(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;
upload.disabled = uploading;
}
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 send(files: File[]): Promise<void> {
try {
await uploadPlanLidar(projectId, files);
} catch (error) {
return showToast(`LAS 올리지 못함 — ${errorText(error)}`, "error");
}
await work.track(follow());
}
/** 올린 뒤 지형 짓기를 끝날 때까지 지켜봄 — 진행 = 서클. */
async function follow(): Promise<void> {
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<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 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 }),
};
}