/* ============================================================================= * 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 } { const selected = new Set(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 { 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 { 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 { 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>): 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 { 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 { 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 { 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(); }