536 lines
18 KiB
TypeScript
536 lines
18 KiB
TypeScript
/* =============================================================================
|
|
* B04_wf1_Surface_UI_Page.ts
|
|
* 로그인 후 04: 1차 워크플로우 (지표면 모델 분석)
|
|
*
|
|
* 3단 레이아웃 (frontend.md §2):
|
|
* 상단: 페이지 타이틀 + 진행 단계 스텝바 (createWorkflowShell)
|
|
* 좌측: 입력 파일 ID + 지면 필터/지표면 표현 선택 + 실행 옵션 폼
|
|
* 우측: 생성된 지표면 모델 목록 그리드
|
|
*
|
|
* 이벤트 핸들러 명명 (frontend.md §4): onB04_Surface_[기능]_[액션]
|
|
* 텍스트는 ui_template_locale에 선(先) 등록 후 참조 (frontend.md §3).
|
|
* ========================================================================== */
|
|
|
|
import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend";
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createTag,
|
|
createWorkflowShell,
|
|
hideLoadingOverlay,
|
|
showLoadingOverlay,
|
|
showToast,
|
|
} from "@ui/ui_template_elements";
|
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
|
import {
|
|
analyzeSurface,
|
|
fetchSurfaceGroundStats,
|
|
fetchSurfacePointCloud,
|
|
fetchSurfaceStatus,
|
|
listSurfaceInputFiles,
|
|
listSurfaceModels,
|
|
type SurfaceInputFileSummary,
|
|
type SurfaceModelSummary,
|
|
type SurfaceStatusResponse,
|
|
} from "./B04_wf1_Surface_Api_Fetch";
|
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
|
import { createSurfacePointCloudViewer } from "./B04_wf1_Surface_UI_Viewer";
|
|
import "./B04_wf1_Surface_UI_Style.css";
|
|
|
|
/** locale 헬퍼 */
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
/** 선택 가능한 지면 필터 (config_system.SURFACE_MODEL_SOURCE_FILTERS + ransac) */
|
|
const SOURCE_FILTERS = ["grid_min_z", "csf", "pmf", "ransac"] as const;
|
|
/** 선택 가능한 지표면 표현 (config_system.SURFACE_MODEL_PRECOMPUTE) */
|
|
const MODEL_METHODS = ["tin", "dtm", "nurbs", "implicit", "meshfree"] as const;
|
|
|
|
/** 체크박스 그룹 하나 생성 (라벨 + 항목들). 선택 값 Set을 반환. */
|
|
function buildCheckboxGroup(
|
|
legend: string,
|
|
values: readonly string[],
|
|
defaults: readonly string[],
|
|
): { root: HTMLElement; selected: Set<string> } {
|
|
const selected = new Set<string>(defaults);
|
|
const root = document.createElement("fieldset");
|
|
root.className = "b04-surface__group";
|
|
const legendEl = document.createElement("legend");
|
|
legendEl.className = "b04-surface__group-legend";
|
|
legendEl.textContent = legend;
|
|
root.append(legendEl);
|
|
|
|
for (const value of values) {
|
|
const item = document.createElement("label");
|
|
item.className = "b04-surface__check";
|
|
const box = document.createElement("input");
|
|
box.type = "checkbox";
|
|
box.value = value;
|
|
box.checked = selected.has(value);
|
|
box.addEventListener("change", () => {
|
|
if (box.checked) selected.add(value);
|
|
else selected.delete(value);
|
|
});
|
|
const text = document.createElement("span");
|
|
text.textContent = value;
|
|
item.append(box, text);
|
|
root.append(item);
|
|
}
|
|
return { root, selected };
|
|
}
|
|
|
|
export function renderB04SurfaceLegacy(root: HTMLElement): void {
|
|
const shell = createWorkflowShell({
|
|
title: L("B04_Surface_Title"),
|
|
steps: workflowSteps(),
|
|
activeStep: 0,
|
|
});
|
|
|
|
/* ---- 좌측 입력 패널 ---- */
|
|
const inputFileField = createInputField({
|
|
label: L("B04_Surface_Field_InputId"),
|
|
type: "number",
|
|
min: 1,
|
|
placeholder: L("B04_Surface_Field_InputId_Placeholder"),
|
|
});
|
|
|
|
const filterGroup = buildCheckboxGroup(L("B04_Surface_Group_Filters"), SOURCE_FILTERS, [
|
|
"grid_min_z",
|
|
"csf",
|
|
"pmf",
|
|
]);
|
|
const methodGroup = buildCheckboxGroup(L("B04_Surface_Group_Methods"), MODEL_METHODS, [
|
|
"dtm",
|
|
"tin",
|
|
]);
|
|
|
|
const forceLabel = document.createElement("label");
|
|
forceLabel.className = "b04-surface__check";
|
|
const forceBox = document.createElement("input");
|
|
forceBox.type = "checkbox";
|
|
const forceText = document.createElement("span");
|
|
forceText.textContent = L("B04_Surface_Field_Force");
|
|
forceLabel.append(forceBox, forceText);
|
|
|
|
const analyzeButton = createButton({
|
|
label: L("B04_Surface_Btn_Analyze"),
|
|
variant: "filled",
|
|
onClick: () => void onB04_Surface_Analyze_Click(),
|
|
});
|
|
|
|
const leftForm = document.createElement("div");
|
|
leftForm.className = "b04-surface__form";
|
|
leftForm.append(
|
|
inputFileField.root,
|
|
filterGroup.root,
|
|
methodGroup.root,
|
|
forceLabel,
|
|
analyzeButton,
|
|
);
|
|
shell.leftPanel.append(leftForm);
|
|
|
|
/* ---- 우측 결과 영역 ---- */
|
|
const resultHeader = document.createElement("div");
|
|
resultHeader.className = "b04-surface__result-head";
|
|
const resultTitle = document.createElement("h3");
|
|
resultTitle.textContent = L("B04_Surface_Result_Title");
|
|
const refreshButton = createButton({
|
|
label: L("B04_Surface_Btn_Refresh"),
|
|
variant: "ghost",
|
|
onClick: () => void onB04_Surface_Refresh_Click(),
|
|
});
|
|
resultHeader.append(resultTitle, refreshButton);
|
|
|
|
const modelList = document.createElement("div");
|
|
modelList.className = "b04-surface__models";
|
|
|
|
const resultArea = document.createElement("div");
|
|
resultArea.className = "b04-surface__result";
|
|
resultArea.append(resultHeader, modelList);
|
|
shell.rightArea.append(resultArea);
|
|
|
|
function renderModels(models: readonly SurfaceModelSummary[]): void {
|
|
modelList.replaceChildren();
|
|
if (models.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "b04-surface__empty";
|
|
empty.textContent = L("B04_Surface_Result_Empty");
|
|
modelList.append(empty);
|
|
return;
|
|
}
|
|
for (const model of models) {
|
|
const card = document.createElement("div");
|
|
card.className = "b04-surface__model-card";
|
|
const head = document.createElement("div");
|
|
head.className = "b04-surface__model-head";
|
|
const type = document.createElement("strong");
|
|
type.textContent = model.model_type;
|
|
const variant =
|
|
model.status === "CONFIRMED" ? "success" : model.status === "FAILED" ? "danger" : "neutral";
|
|
head.append(type, createTag(model.status, variant));
|
|
|
|
const meta = document.createElement("div");
|
|
meta.className = "b04-surface__model-meta";
|
|
const resolution = document.createElement("span");
|
|
resolution.textContent = `${L("B04_Surface_Model_Resolution")}: ${model.resolution_m ?? "-"}`;
|
|
const path = document.createElement("span");
|
|
path.textContent = `${L("B04_Surface_Model_Path")}: ${model.model_file_path ?? "-"}`;
|
|
meta.append(resolution, path);
|
|
|
|
card.append(head, meta);
|
|
modelList.append(card);
|
|
}
|
|
}
|
|
|
|
function getProjectId(): string | null {
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
if (!projectId) {
|
|
inputFileField.setError(L("B04_Surface_Error_Project"));
|
|
showToast(L("B04_Surface_Error_Project"), "error");
|
|
}
|
|
return projectId;
|
|
}
|
|
|
|
async function onB04_Surface_Analyze_Click(): Promise<void> {
|
|
const projectId = getProjectId();
|
|
if (!projectId) return;
|
|
|
|
const rawId = inputFileField.input.value.trim();
|
|
const inputFileId = Number(rawId);
|
|
if (!rawId || !Number.isInteger(inputFileId) || inputFileId <= 0) {
|
|
inputFileField.setError(L("B04_Surface_Error_InputId"));
|
|
return;
|
|
}
|
|
if (filterGroup.selected.size === 0 || methodGroup.selected.size === 0) {
|
|
inputFileField.setError(L("B04_Surface_Error_Selection"));
|
|
return;
|
|
}
|
|
inputFileField.setError();
|
|
|
|
showLoadingOverlay();
|
|
try {
|
|
const response = await analyzeSurface(projectId, {
|
|
input_file_id: inputFileId,
|
|
source_filters: [...filterGroup.selected],
|
|
methods: [...methodGroup.selected],
|
|
force: forceBox.checked,
|
|
});
|
|
showToast(
|
|
`${L("B04_Surface_Analyze_Success")} (${response.surface_model_ids.length})`,
|
|
"success",
|
|
);
|
|
await loadModels(projectId);
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : L("B04_Surface_Analyze_Failed");
|
|
inputFileField.setError(`${L("B04_Surface_Analyze_Failed")} ${detail}`);
|
|
showToast(L("B04_Surface_Analyze_Failed"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
async function loadModels(projectId: string): Promise<void> {
|
|
showLoadingOverlay();
|
|
try {
|
|
const response = await listSurfaceModels(projectId);
|
|
renderModels(response.models);
|
|
} catch {
|
|
showToast(L("B04_Surface_Load_Failed"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
async function onB04_Surface_Refresh_Click(): Promise<void> {
|
|
const projectId = getProjectId();
|
|
if (projectId) await loadModels(projectId);
|
|
}
|
|
|
|
renderModels([]);
|
|
root.replaceChildren(shell.root);
|
|
|
|
const initialProjectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
if (initialProjectId) void loadModels(initialProjectId);
|
|
}
|
|
|
|
function buildInfoLine(label: string, value: unknown): HTMLElement {
|
|
const row = document.createElement("div");
|
|
row.className = "b04-surface__line";
|
|
const key = document.createElement("span");
|
|
key.textContent = label;
|
|
const val = document.createElement("strong");
|
|
val.textContent = value == null || value === "" ? "-" : String(value);
|
|
row.append(key, val);
|
|
return row;
|
|
}
|
|
|
|
export function renderB04Surface(root: HTMLElement): void {
|
|
let selectedInputFile: SurfaceInputFileSummary | null = null;
|
|
let inputFiles: SurfaceInputFileSummary[] = [];
|
|
|
|
const inputSelect = document.createElement("select");
|
|
inputSelect.className = "b04-surface__select";
|
|
const inputInfo = document.createElement("div");
|
|
inputInfo.className = "b04-surface__input-info";
|
|
const statusBox = document.createElement("div");
|
|
statusBox.className = "b04-surface__status";
|
|
const modelList = document.createElement("div");
|
|
modelList.className = "b04-surface__models";
|
|
const statsList = document.createElement("div");
|
|
statsList.className = "b04-surface__stats";
|
|
const viewer = createSurfacePointCloudViewer();
|
|
|
|
const filterGroup = buildCheckboxGroup(L("B04_Surface_Group_Filters"), SOURCE_FILTERS, [
|
|
"grid_min_z",
|
|
"csf",
|
|
"pmf",
|
|
]);
|
|
const methodGroup = buildCheckboxGroup(L("B04_Surface_Group_Methods"), MODEL_METHODS, [
|
|
"dtm",
|
|
"tin",
|
|
]);
|
|
|
|
const forceLabel = document.createElement("label");
|
|
forceLabel.className = "b04-surface__check";
|
|
const forceBox = document.createElement("input");
|
|
forceBox.type = "checkbox";
|
|
const forceText = document.createElement("span");
|
|
forceText.textContent = L("B04_Surface_Field_Force");
|
|
forceLabel.append(forceBox, forceText);
|
|
|
|
const analyzeButton = createButton({
|
|
label: L("B04_Surface_Btn_Analyze"),
|
|
variant: "filled",
|
|
onClick: () => void onB04_Surface_Analyze_Click(),
|
|
});
|
|
const refreshButton = createButton({
|
|
label: L("B04_Surface_Btn_Refresh"),
|
|
variant: "ghost",
|
|
onClick: () => void onB04_Surface_Refresh_Click(),
|
|
});
|
|
|
|
const panel = document.createElement("div");
|
|
panel.className = "b04-surface__form";
|
|
const inputGroup = document.createElement("section");
|
|
inputGroup.className = "b04-surface__group";
|
|
const inputTitle = document.createElement("h3");
|
|
inputTitle.className = "b04-surface__panel-title";
|
|
inputTitle.textContent = L("B04_Surface_InputFiles");
|
|
inputGroup.append(inputTitle, inputSelect, inputInfo);
|
|
panel.append(
|
|
inputGroup,
|
|
filterGroup.root,
|
|
methodGroup.root,
|
|
forceLabel,
|
|
analyzeButton,
|
|
refreshButton,
|
|
);
|
|
|
|
const workspace = document.createElement("div");
|
|
workspace.className = "b04-surface__workspace";
|
|
const topbar = document.createElement("div");
|
|
topbar.className = "b04-surface__topbar";
|
|
const title = document.createElement("h3");
|
|
title.textContent = L("B04_Surface_PointCloud_Title");
|
|
topbar.append(title, statusBox);
|
|
|
|
const bottom = document.createElement("div");
|
|
bottom.className = "b04-surface__bottom";
|
|
const statsSection = document.createElement("section");
|
|
statsSection.className = "b04-surface__section";
|
|
const statsTitle = document.createElement("h3");
|
|
statsTitle.textContent = L("B04_Surface_GroundStats_Title");
|
|
statsSection.append(statsTitle, statsList);
|
|
const modelsSection = document.createElement("section");
|
|
modelsSection.className = "b04-surface__section";
|
|
const modelsTitle = document.createElement("h3");
|
|
modelsTitle.textContent = L("B04_Surface_Result_Title");
|
|
modelsSection.append(modelsTitle, modelList);
|
|
bottom.append(statsSection, modelsSection);
|
|
workspace.append(topbar, viewer.root, bottom);
|
|
|
|
const layout = createWorkflowLayout({
|
|
title: L("B04_Surface_Title"),
|
|
steps: workflowSteps(),
|
|
activeStep: 1,
|
|
leftPanel: panel,
|
|
mainContent: workspace,
|
|
});
|
|
|
|
function getProjectId(): string | null {
|
|
const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY);
|
|
if (!projectId) showToast(L("B04_Surface_Error_Project"), "error");
|
|
return projectId;
|
|
}
|
|
|
|
function renderStatus(status: SurfaceStatusResponse | null): void {
|
|
statusBox.replaceChildren();
|
|
if (!status) {
|
|
statusBox.append(createTag(L("B04_Surface_Status_Unknown"), "neutral"));
|
|
return;
|
|
}
|
|
const variant =
|
|
status.status === "completed" ? "success" : status.status === "failed" ? "danger" : "warning";
|
|
statusBox.append(
|
|
createTag(`${status.progress_percent}%`, variant),
|
|
document.createTextNode(status.message),
|
|
);
|
|
}
|
|
|
|
function renderInputFiles(files: readonly SurfaceInputFileSummary[]): void {
|
|
inputFiles = [...files];
|
|
inputSelect.replaceChildren();
|
|
if (inputFiles.length === 0) {
|
|
selectedInputFile = null;
|
|
const option = document.createElement("option");
|
|
option.value = "";
|
|
option.textContent = L("B04_Surface_InputFiles_Empty");
|
|
inputSelect.append(option);
|
|
inputInfo.replaceChildren();
|
|
analyzeButton.disabled = true;
|
|
return;
|
|
}
|
|
selectedInputFile = inputFiles[0];
|
|
for (const file of inputFiles) {
|
|
const option = document.createElement("option");
|
|
option.value = String(file.id);
|
|
option.textContent = `${file.original_filename} (#${file.id})`;
|
|
inputSelect.append(option);
|
|
}
|
|
inputSelect.value = String(selectedInputFile.id);
|
|
analyzeButton.disabled = false;
|
|
renderInputInfo();
|
|
}
|
|
|
|
function renderInputInfo(): void {
|
|
inputInfo.replaceChildren();
|
|
if (!selectedInputFile) return;
|
|
inputInfo.append(
|
|
buildInfoLine(L("B04_Surface_Input_FileName"), selectedInputFile.original_filename),
|
|
buildInfoLine(L("B04_Surface_Input_Crs"), selectedInputFile.crs_epsg),
|
|
buildInfoLine(L("B04_Surface_Input_Size"), selectedInputFile.file_size_mb?.toFixed(2)),
|
|
);
|
|
}
|
|
|
|
function renderModels(models: readonly SurfaceModelSummary[]): void {
|
|
modelList.replaceChildren();
|
|
if (models.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "b04-surface__empty";
|
|
empty.textContent = L("B04_Surface_Result_Empty");
|
|
modelList.append(empty);
|
|
return;
|
|
}
|
|
for (const model of models) {
|
|
const card = document.createElement("article");
|
|
card.className = "b04-surface__model-card";
|
|
const head = document.createElement("div");
|
|
head.className = "b04-surface__model-head";
|
|
const type = document.createElement("strong");
|
|
type.textContent = model.model_type;
|
|
const variant =
|
|
model.status === "CONFIRMED" || model.status === "COMPLETE" ? "success" : "neutral";
|
|
head.append(type, createTag(model.status, variant));
|
|
const meta = document.createElement("div");
|
|
meta.className = "b04-surface__model-meta";
|
|
meta.append(
|
|
buildInfoLine(L("B04_Surface_Model_Resolution"), model.resolution_m),
|
|
buildInfoLine(L("B04_Surface_Model_Path"), model.model_file_path),
|
|
);
|
|
card.append(head, meta);
|
|
modelList.append(card);
|
|
}
|
|
}
|
|
|
|
function renderGroundStats(filters: Record<string, Record<string, unknown>>): void {
|
|
statsList.replaceChildren();
|
|
const entries = Object.entries(filters);
|
|
if (entries.length === 0) {
|
|
const empty = document.createElement("p");
|
|
empty.className = "b04-surface__empty";
|
|
empty.textContent = L("B04_Surface_GroundStats_Empty");
|
|
statsList.append(empty);
|
|
return;
|
|
}
|
|
for (const [name, value] of entries) {
|
|
const item = document.createElement("article");
|
|
item.className = "b04-surface__stat-card";
|
|
item.append(
|
|
buildInfoLine(L("B04_Surface_GroundStats_Filter"), name),
|
|
buildInfoLine(L("B04_Surface_GroundStats_SourcePoints"), value.source_point_count),
|
|
);
|
|
statsList.append(item);
|
|
}
|
|
}
|
|
|
|
async function loadProjectData(projectId: string): Promise<void> {
|
|
const [inputs, status, models, stats] = await Promise.all([
|
|
listSurfaceInputFiles(projectId),
|
|
fetchSurfaceStatus(projectId),
|
|
listSurfaceModels(projectId),
|
|
fetchSurfaceGroundStats(projectId),
|
|
]);
|
|
renderInputFiles(inputs.files);
|
|
renderStatus(status);
|
|
renderModels(models.models);
|
|
renderGroundStats(stats.filters);
|
|
try {
|
|
viewer.render(await fetchSurfacePointCloud(projectId));
|
|
} catch {
|
|
viewer.render(null);
|
|
}
|
|
}
|
|
|
|
async function onB04_Surface_Analyze_Click(): Promise<void> {
|
|
const projectId = getProjectId();
|
|
if (!projectId || !selectedInputFile) return;
|
|
if (filterGroup.selected.size === 0 || methodGroup.selected.size === 0) {
|
|
showToast(L("B04_Surface_Error_Selection"), "error");
|
|
return;
|
|
}
|
|
showLoadingOverlay();
|
|
try {
|
|
const response = await analyzeSurface(projectId, {
|
|
input_file_id: selectedInputFile.id,
|
|
source_filters: [...filterGroup.selected],
|
|
methods: [...methodGroup.selected],
|
|
force: forceBox.checked,
|
|
});
|
|
showToast(
|
|
`${L("B04_Surface_Analyze_Success")} (${response.surface_model_ids.length})`,
|
|
"success",
|
|
);
|
|
await loadProjectData(projectId);
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : L("B04_Surface_Analyze_Failed");
|
|
showToast(`${L("B04_Surface_Analyze_Failed")} ${detail}`, "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
async function onB04_Surface_Refresh_Click(): Promise<void> {
|
|
const projectId = getProjectId();
|
|
if (!projectId) return;
|
|
showLoadingOverlay();
|
|
try {
|
|
await loadProjectData(projectId);
|
|
} catch {
|
|
showToast(L("B04_Surface_Load_Failed"), "error");
|
|
} finally {
|
|
hideLoadingOverlay();
|
|
}
|
|
}
|
|
|
|
inputSelect.addEventListener("change", () => {
|
|
selectedInputFile = inputFiles.find((file) => String(file.id) === inputSelect.value) ?? null;
|
|
renderInputInfo();
|
|
});
|
|
|
|
root.replaceChildren(layout.root);
|
|
const projectId = getProjectId();
|
|
if (projectId) void onB04_Surface_Refresh_Click();
|
|
}
|