- 공용 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>
176 lines
7.7 KiB
TypeScript
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,
|
|
};
|
|
}
|