Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Terrain.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

176 lines
7.7 KiB
TypeScript

/* =============================================================================
* 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<RouteLidar>({
path: lidarPath,
poll: () => requestJson<RouteLidar>(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<void> {
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<void> {
const info = await requestJson<RouteLidar>(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<void> {
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,
};
}