Merge remote-tracking branch 'origin/main' into sub_desktop_1
This commit is contained in:
@@ -191,6 +191,30 @@ export async function analyzeSurface(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 도엽 보간 방식 목록 — `built`가 false면 아직 만들지 않은 방식이다. */
|
||||||
|
export interface SheetMethodListResponse {
|
||||||
|
status: string;
|
||||||
|
methods: Array<{ key: string; label: string; built: boolean }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSheetMethods(projectId: string): Promise<SheetMethodListResponse> {
|
||||||
|
return requestJson<SheetMethodListResponse>(`/projects/${projectId}/surface/sheet-methods`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 도엽 서피스 한 방식을 만들어 등록한다. 조합 생성이라 타임아웃을 길게 준다. */
|
||||||
|
export async function buildSheetSurface(
|
||||||
|
projectId: string,
|
||||||
|
method: string,
|
||||||
|
): Promise<{ status: string; method: string; surface_model_ids: number[] }> {
|
||||||
|
return requestJson(
|
||||||
|
`/projects/${projectId}/surface/sheet-surface`,
|
||||||
|
{ method: "POST", body: JSON.stringify({ method }) },
|
||||||
|
API_SURFACE_BUILD_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 프로젝트의 지표면 모델 목록을 조회한다. */
|
/** 프로젝트의 지표면 모델 목록을 조회한다. */
|
||||||
export async function listSurfaceModels(projectId: string): Promise<SurfaceModelListResponse> {
|
export async function listSurfaceModels(projectId: string): Promise<SurfaceModelListResponse> {
|
||||||
return requestJson<SurfaceModelListResponse>(`/projects/${projectId}/surface/models`, {
|
return requestJson<SurfaceModelListResponse>(`/projects/${projectId}/surface/models`, {
|
||||||
|
|||||||
@@ -389,3 +389,61 @@ async def save_surface_analysis_to_db(
|
|||||||
statistics=None,
|
statistics=None,
|
||||||
)
|
)
|
||||||
return surface_model_ids
|
return surface_model_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def save_sheet_surface_models(
|
||||||
|
connection: aiomysql.Connection, project_id: UUID, models: list[dict[str, Any]]
|
||||||
|
) -> list[int]:
|
||||||
|
"""관리자가 요청해 새로 만든 도엽 서피스 모델을 등록한다 (2026-09-01).
|
||||||
|
|
||||||
|
전체 재분석(`save_surface_analysis_to_db`)과 달리 **기존 행을 지우지 않는다** —
|
||||||
|
방식 하나만 덧붙이는 요청이라 이미 있는 모델을 날리면 안 된다. 같은 파일을 가리키는
|
||||||
|
옛 행만 걷어 낸다. 입력 파일·구조화 클라우드·좌표계는 같은 프로젝트의 기존 모델에서
|
||||||
|
물려받는다(도엽 서피스는 LAS가 아니라 등고선에서 나오지만 같은 사업지다).
|
||||||
|
|
||||||
|
이 함수는 트랜잭션을 시작하거나 끝내지 않는다 — 호출자가 한 번만 한다.
|
||||||
|
"""
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT source_file_id, processed_cloud_id, crs_epsg
|
||||||
|
FROM surface_models WHERE project_id = %s ORDER BY id LIMIT 1
|
||||||
|
""",
|
||||||
|
(str(project_id),),
|
||||||
|
)
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
source_file_id, processed_cloud_id, crs_epsg = row if row else (None, None, None)
|
||||||
|
|
||||||
|
model_ids: list[int] = []
|
||||||
|
for model in models:
|
||||||
|
model_path = model.get("model_file_path")
|
||||||
|
if model_path:
|
||||||
|
async with connection.cursor() as cursor:
|
||||||
|
await cursor.execute(
|
||||||
|
"DELETE FROM surface_models WHERE project_id = %s AND model_file_path = %s",
|
||||||
|
(str(project_id), model_path),
|
||||||
|
)
|
||||||
|
model_id = await create_surface_model(
|
||||||
|
connection,
|
||||||
|
project_id=project_id,
|
||||||
|
model_type=model["model_type"],
|
||||||
|
source_file_id=source_file_id,
|
||||||
|
processed_cloud_id=processed_cloud_id,
|
||||||
|
crs_epsg=crs_epsg,
|
||||||
|
resolution_m=model["resolution_m"],
|
||||||
|
model_file_path=model_path,
|
||||||
|
generation_params=model["generation_params"],
|
||||||
|
)
|
||||||
|
model_ids.append(model_id)
|
||||||
|
for layer in model.get("layers", []):
|
||||||
|
await create_terrain_layer(
|
||||||
|
connection,
|
||||||
|
surface_model_id=model_id,
|
||||||
|
layer_name=layer["layer_name"],
|
||||||
|
geometry_type=layer["geometry_type"],
|
||||||
|
layer_file_path=layer["file_path"],
|
||||||
|
file_format=layer["file_format"],
|
||||||
|
file_size_mb=None,
|
||||||
|
statistics=None,
|
||||||
|
)
|
||||||
|
return model_ids
|
||||||
|
|||||||
@@ -277,3 +277,75 @@ async def get_planned_route(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("계획노선 조회 실패: %s", exc)
|
logger.warning("계획노선 조회 실패: %s", exc)
|
||||||
return JSONResponse(status_code=500, content={"status": "error", "message": str(exc)})
|
return JSONResponse(status_code=500, content={"status": "error", "message": str(exc)})
|
||||||
|
|
||||||
|
|
||||||
|
# ── 도엽등고 서피스 보간 방식 (2026-09-01) ────────────────────────────────
|
||||||
|
# 자동 전처리는 기본 방식 하나만 만든다. 나머지는 관리자가 화면에서 그 방식을
|
||||||
|
# 고를 때 여기로 요청해 만든다. 파일명이 방식마다 갈려 있어(`dtm_sheet_{방식}.npz`)
|
||||||
|
# 한 번 만든 방식은 다시 골라도 그대로 쓰인다.
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{project_id}/surface/sheet-methods", response_model=None)
|
||||||
|
async def list_sheet_methods(project_id: UUID) -> dict[str, Any]:
|
||||||
|
"""고를 수 있는 도엽 보간 방식과, 이미 만들어 둔 방식을 알려준다."""
|
||||||
|
from B04_PreProcess.B04_PreProcess_Engine_SheetMethods import SHEET_METHOD_LABELS
|
||||||
|
from config.config_system import SHEET_SURFACE_METHODS
|
||||||
|
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||||
|
models_dir = Path(resolve_stored_project_path(stored_path)) / "B04_PreProcess" / "models"
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"label": SHEET_METHOD_LABELS.get(key, key),
|
||||||
|
"built": (models_dir / f"dtm_sheet_{key}.npz").is_file(),
|
||||||
|
}
|
||||||
|
for key in SHEET_SURFACE_METHODS
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{project_id}/surface/sheet-surface", response_model=None)
|
||||||
|
async def build_sheet_surface(project_id: UUID, request: Request) -> dict[str, Any] | JSONResponse:
|
||||||
|
"""도엽 서피스 한 방식을 만들어 DB에 등록한다. 이미 있으면 그대로 둔다."""
|
||||||
|
from B04_PreProcess.B04_PreProcess_Engine_SheetMethods import SHEET_METHOD_BUILDERS
|
||||||
|
from B04_PreProcess.B04_PreProcess_Engine_SheetSurface import build_sheet_surface_from_route
|
||||||
|
from B04_PreProcess.B04_PreProcess_Repository import save_sheet_surface_models
|
||||||
|
|
||||||
|
body = await request.json()
|
||||||
|
method = str(body.get("method") or "")
|
||||||
|
if method not in SHEET_METHOD_BUILDERS:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"status": "error", "message": f"지원하지 않는 보간 방식입니다: {method}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||||
|
project_root = Path(resolve_stored_project_path(stored_path))
|
||||||
|
stage_root = project_root / "B04_PreProcess"
|
||||||
|
models = await asyncio.to_thread(
|
||||||
|
build_sheet_surface_from_route,
|
||||||
|
project_root,
|
||||||
|
stage_root / "processed",
|
||||||
|
stage_root / "models",
|
||||||
|
[method],
|
||||||
|
)
|
||||||
|
if not models:
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=400,
|
||||||
|
content={"status": "error", "message": "도엽 서피스를 만들지 못했습니다."},
|
||||||
|
)
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
await connection.begin()
|
||||||
|
try:
|
||||||
|
model_ids = await save_sheet_surface_models(connection, project_id, models)
|
||||||
|
await connection.commit()
|
||||||
|
except Exception:
|
||||||
|
await connection.rollback()
|
||||||
|
raise
|
||||||
|
return {"status": "success", "method": method, "surface_model_ids": model_ids}
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import {
|
|||||||
} from "@ui/ui_template_elements";
|
} from "@ui/ui_template_elements";
|
||||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||||
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
import { fetchDashboardMe } from "../B01_Dashboard/B01_Dashboard_Api_Fetch";
|
||||||
import {
|
import { clearPreloadMark, purgeOtherProjects } from "../A00_Common/b_asset_cache";
|
||||||
clearPreloadMark,
|
|
||||||
purgeOtherProjects,
|
|
||||||
} from "../A00_Common/b_asset_cache";
|
|
||||||
import { clearRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
|
import { clearRouteLatestCache } from "../B05_Profile/B05_Profile_Api_Fetch";
|
||||||
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
import { workflowSteps } from "../A00_Common/b_page_scaffold";
|
||||||
import {
|
import {
|
||||||
@@ -24,11 +21,13 @@ import {
|
|||||||
} from "../A00_Common/b_workflow_nav";
|
} from "../A00_Common/b_workflow_nav";
|
||||||
import {
|
import {
|
||||||
analyzeSurface,
|
analyzeSurface,
|
||||||
|
buildSheetSurface,
|
||||||
confirmSurfaceModel,
|
confirmSurfaceModel,
|
||||||
fetchConfirmedSurface,
|
fetchConfirmedSurface,
|
||||||
fetchPlannedRoute,
|
fetchPlannedRoute,
|
||||||
fetchSurfacePointCloud,
|
fetchSurfacePointCloud,
|
||||||
fetchSurfaceStatus,
|
fetchSurfaceStatus,
|
||||||
|
listSheetMethods,
|
||||||
listSurfaceInputFiles,
|
listSurfaceInputFiles,
|
||||||
listSurfaceModels,
|
listSurfaceModels,
|
||||||
type SurfaceInputFileSummary,
|
type SurfaceInputFileSummary,
|
||||||
@@ -49,14 +48,7 @@ const DEFAULT_FILTER = "csf";
|
|||||||
const DEFAULT_METHOD = "dtm";
|
const DEFAULT_METHOD = "dtm";
|
||||||
const ROUTE_STAGE = ROUTES.B05_PROFILE;
|
const ROUTE_STAGE = ROUTES.B05_PROFILE;
|
||||||
// 도엽 서피스 보간 방식 버튼 순서 — 백엔드 SHEET_SURFACE_METHODS와 같은 차례로 둔다.
|
// 도엽 서피스 보간 방식 버튼 순서 — 백엔드 SHEET_SURFACE_METHODS와 같은 차례로 둔다.
|
||||||
const SHEET_METHOD_ORDER = [
|
const SHEET_METHOD_ORDER = ["tin_sheet", "tin", "biharmonic", "anudem", "multires", "laplace"];
|
||||||
"tin_sheet",
|
|
||||||
"tin",
|
|
||||||
"biharmonic",
|
|
||||||
"anudem",
|
|
||||||
"multires",
|
|
||||||
"laplace",
|
|
||||||
];
|
|
||||||
|
|
||||||
function L(key: keyof typeof ui_locales): string {
|
function L(key: keyof typeof ui_locales): string {
|
||||||
return ui_locales[key][currentLanguageIndex];
|
return ui_locales[key][currentLanguageIndex];
|
||||||
@@ -122,17 +114,11 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
if (guardedProjectId) {
|
if (guardedProjectId) {
|
||||||
const user = await fetchDashboardMe();
|
const user = await fetchDashboardMe();
|
||||||
if (user.role !== "SYSTEM_ADMIN") {
|
if (user.role !== "SYSTEM_ADMIN") {
|
||||||
const workflowState = await fetchWorkflowState(guardedProjectId).catch(
|
const workflowState = await fetchWorkflowState(guardedProjectId).catch(() => undefined);
|
||||||
() => undefined,
|
const surfaceStage = workflowState?.stages.find((stage) => stage.stage_no === 1);
|
||||||
);
|
|
||||||
const surfaceStage = workflowState?.stages.find(
|
|
||||||
(stage) => stage.stage_no === 1,
|
|
||||||
);
|
|
||||||
goToWorkflowStage(
|
goToWorkflowStage(
|
||||||
guardedProjectId,
|
guardedProjectId,
|
||||||
surfaceStage?.state === "COMPLETE"
|
surfaceStage?.state === "COMPLETE" ? ROUTES.B05_PROFILE : ROUTES.B03_FILE_INPUT,
|
||||||
? ROUTES.B05_PROFILE
|
|
||||||
: ROUTES.B03_FILE_INPUT,
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -192,15 +178,54 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
sheetViewer.render(projectId, models);
|
sheetViewer.render(projectId, models);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 아직 안 만든 도엽 방식이면 물어보고 그 방식만 만든다. 만들어 두면 다시 골라도 그대로 쓴다. */
|
||||||
|
async function onSheetMethodClick(method: string, label: string): Promise<void> {
|
||||||
|
const projectId = getProjectId();
|
||||||
|
if (!projectId) return;
|
||||||
|
const built = models.some(
|
||||||
|
(model) =>
|
||||||
|
model.model_type.toLowerCase() === "dtm" && getModelFilter(model) === `sheet_${method}`,
|
||||||
|
);
|
||||||
|
if (built) {
|
||||||
|
selectSheetMethod(method);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = L("B04_Surface_Build_Confirm")
|
||||||
|
.replace("{filter}", label)
|
||||||
|
.replace("{method}", "도엽등고");
|
||||||
|
if (!(await showConfirmDialog(message, L("B04_Surface_Build_Action")))) return;
|
||||||
|
|
||||||
|
showLoadingOverlay();
|
||||||
|
showToast(L("B04_Surface_Build_Running"), "info");
|
||||||
|
try {
|
||||||
|
await buildSheetSurface(projectId, method);
|
||||||
|
models = (await listSurfaceModels(projectId)).models;
|
||||||
|
// 만들었으니 「미생성」 표시를 걷는다 — 버튼 줄을 통째로 다시 세우지는 않는다
|
||||||
|
// (현재 선택·스무딩 상태가 날아간다).
|
||||||
|
const button = sheetMethodButtons.get(method);
|
||||||
|
if (button) {
|
||||||
|
button.classList.remove("is-unbuilt");
|
||||||
|
button.removeAttribute("title");
|
||||||
|
}
|
||||||
|
showToast(
|
||||||
|
L("B04_Surface_Build_Done").replace("{filter}", label).replace("{method}", "도엽등고"),
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
selectSheetMethod(method);
|
||||||
|
} catch (error) {
|
||||||
|
const detail = error instanceof Error ? error.message : "";
|
||||||
|
showToast(`${L("B04_Surface_Build_Failed")} ${detail}`, "error");
|
||||||
|
} finally {
|
||||||
|
hideLoadingOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 라이다 지표면 겹쳐 보기 — 확정 필터의 DTM을 반투명으로 얹는다.
|
// 라이다 지표면 겹쳐 보기 — 확정 필터의 DTM을 반투명으로 얹는다.
|
||||||
const lidarLabel = document.createElement("label");
|
const lidarLabel = document.createElement("label");
|
||||||
lidarLabel.className = "toggle-label toggle-button b04-surface__sheet-lidar";
|
lidarLabel.className = "toggle-label toggle-button b04-surface__sheet-lidar";
|
||||||
const lidarCheck = document.createElement("input");
|
const lidarCheck = document.createElement("input");
|
||||||
lidarCheck.type = "checkbox";
|
lidarCheck.type = "checkbox";
|
||||||
lidarLabel.append(
|
lidarLabel.append(lidarCheck, document.createTextNode(` ${L("B04_Surface_SheetLidar")}`));
|
||||||
lidarCheck,
|
|
||||||
document.createTextNode(` ${L("B04_Surface_SheetLidar")}`),
|
|
||||||
);
|
|
||||||
lidarCheck.addEventListener("change", () => {
|
lidarCheck.addEventListener("change", () => {
|
||||||
void sheetViewer
|
void sheetViewer
|
||||||
.showOverlay(
|
.showOverlay(
|
||||||
@@ -278,13 +303,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
const actionRow = document.createElement("div");
|
const actionRow = document.createElement("div");
|
||||||
actionRow.className = "ui-sidebar-actions";
|
actionRow.className = "ui-sidebar-actions";
|
||||||
actionRow.append(confirmButton, resetButton);
|
actionRow.append(confirmButton, resetButton);
|
||||||
panel.append(
|
panel.append(inputGroup, analysisGroup, displayGroup, viewer.controlsGroup, actionRow);
|
||||||
inputGroup,
|
|
||||||
analysisGroup,
|
|
||||||
displayGroup,
|
|
||||||
viewer.controlsGroup,
|
|
||||||
actionRow,
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewers = document.createElement("div");
|
const viewers = document.createElement("div");
|
||||||
viewers.className = "b04-surface__viewers";
|
viewers.className = "b04-surface__viewers";
|
||||||
@@ -313,8 +332,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
currentStage: workflowState?.current_stage,
|
currentStage: workflowState?.current_stage,
|
||||||
routes: WORKFLOW_STEP_ROUTES,
|
routes: WORKFLOW_STEP_ROUTES,
|
||||||
onStepClick: (stepIndex) => {
|
onStepClick: (stepIndex) => {
|
||||||
if (layoutProjectId)
|
if (layoutProjectId) goToWorkflowStage(layoutProjectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
||||||
goToWorkflowStage(layoutProjectId, WORKFLOW_STEP_ROUTES[stepIndex]);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -348,11 +366,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const variant =
|
const variant =
|
||||||
status.status === "completed"
|
status.status === "completed" ? "success" : status.status === "failed" ? "danger" : "warning";
|
||||||
? "success"
|
|
||||||
: status.status === "failed"
|
|
||||||
? "danger"
|
|
||||||
: "warning";
|
|
||||||
statusBox.append(
|
statusBox.append(
|
||||||
createTag(`${status.progress_percent}%`, variant),
|
createTag(`${status.progress_percent}%`, variant),
|
||||||
document.createTextNode(status.message),
|
document.createTextNode(status.message),
|
||||||
@@ -369,9 +383,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
inputInfo.append(
|
inputInfo.append(
|
||||||
buildInfoLine(
|
buildInfoLine(
|
||||||
"좌표계",
|
"좌표계",
|
||||||
selectedInputFile.crs_epsg
|
selectedInputFile.crs_epsg ? `EPSG:${selectedInputFile.crs_epsg}` : null,
|
||||||
? `EPSG:${selectedInputFile.crs_epsg}`
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
buildInfoLine(
|
buildInfoLine(
|
||||||
"크기",
|
"크기",
|
||||||
@@ -380,10 +392,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
: `${selectedInputFile.file_size_mb.toFixed(2)} MB`,
|
: `${selectedInputFile.file_size_mb.toFixed(2)} MB`,
|
||||||
),
|
),
|
||||||
buildInfoLine("포인트 수", pointCloud?.point_count.toLocaleString()),
|
buildInfoLine("포인트 수", pointCloud?.point_count.toLocaleString()),
|
||||||
buildInfoLine(
|
buildInfoLine("표시 포인트 수", pointCloud?.sampled_count.toLocaleString()),
|
||||||
"표시 포인트 수",
|
|
||||||
pointCloud?.sampled_count.toLocaleString(),
|
|
||||||
),
|
|
||||||
buildInfoLine("높이 범위", heightRange),
|
buildInfoLine("높이 범위", heightRange),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -453,9 +462,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
});
|
});
|
||||||
models = (await listSurfaceModels(projectId)).models;
|
models = (await listSurfaceModels(projectId)).models;
|
||||||
showToast(
|
showToast(
|
||||||
L("B04_Surface_Build_Done")
|
L("B04_Surface_Build_Done").replace("{filter}", filter).replace("{method}", method),
|
||||||
.replace("{filter}", filter)
|
|
||||||
.replace("{method}", method),
|
|
||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
@@ -473,10 +480,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
function updateSelectedModel(): void {
|
function updateSelectedModel(): void {
|
||||||
const projectId = getProjectId();
|
const projectId = getProjectId();
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
terrainViewer.setSelection(
|
terrainViewer.setSelection(filterGroup.select.value, methodGroup.select.value);
|
||||||
filterGroup.select.value,
|
|
||||||
methodGroup.select.value,
|
|
||||||
);
|
|
||||||
terrainViewer.render(projectId, models);
|
terrainViewer.render(projectId, models);
|
||||||
confirmButton.disabled = !findSelectedModel();
|
confirmButton.disabled = !findSelectedModel();
|
||||||
}
|
}
|
||||||
@@ -487,20 +491,14 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
showLoadingOverlay();
|
showLoadingOverlay();
|
||||||
viewer.setLoading("포인트 데이터 로딩 중…");
|
viewer.setLoading("포인트 데이터 로딩 중…");
|
||||||
try {
|
try {
|
||||||
pointCloud = await fetchSurfacePointCloud(
|
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
|
||||||
projectId,
|
|
||||||
filterGroup.select.value,
|
|
||||||
);
|
|
||||||
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
||||||
viewer.render(pointCloud);
|
viewer.render(pointCloud);
|
||||||
renderInputInfo();
|
renderInputInfo();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pointCloud = null;
|
pointCloud = null;
|
||||||
viewer.render(null);
|
viewer.render(null);
|
||||||
const detail =
|
const detail = error instanceof Error ? error.message : "지면 포인트 조회에 실패했습니다.";
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "지면 포인트 조회에 실패했습니다.";
|
|
||||||
showToast(detail, "error");
|
showToast(detail, "error");
|
||||||
} finally {
|
} finally {
|
||||||
hideLoadingOverlay();
|
hideLoadingOverlay();
|
||||||
@@ -520,8 +518,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
// 확정본과 같은 조합에서 시작해야 B05와 같은 파일을 보고, 보관함도 한 벌만 쓴다.
|
// 확정본과 같은 조합에서 시작해야 B05와 같은 파일을 보고, 보관함도 한 벌만 쓴다.
|
||||||
// 확정 이력이 없을 때만 개발 기본값(csf·dtm)으로 둔다.
|
// 확정 이력이 없을 때만 개발 기본값(csf·dtm)으로 둔다.
|
||||||
if (confirmed.model_id) {
|
if (confirmed.model_id) {
|
||||||
if (confirmed.source_filter)
|
if (confirmed.source_filter) filterGroup.select.value = confirmed.source_filter;
|
||||||
filterGroup.select.value = confirmed.source_filter;
|
|
||||||
if (confirmed.method) methodGroup.select.value = confirmed.method;
|
if (confirmed.method) methodGroup.select.value = confirmed.method;
|
||||||
terrainViewer.setSmoothing(confirmed.smooth ?? false);
|
terrainViewer.setSmoothing(confirmed.smooth ?? false);
|
||||||
}
|
}
|
||||||
@@ -539,10 +536,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
.catch(() => terrainViewer.setRoute([]));
|
.catch(() => terrainViewer.setRoute([]));
|
||||||
viewer.setLoading("포인트 데이터 로딩 중…");
|
viewer.setLoading("포인트 데이터 로딩 중…");
|
||||||
try {
|
try {
|
||||||
pointCloud = await fetchSurfacePointCloud(
|
pointCloud = await fetchSurfacePointCloud(projectId, filterGroup.select.value);
|
||||||
projectId,
|
|
||||||
filterGroup.select.value,
|
|
||||||
);
|
|
||||||
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
terrainViewer.setReferenceBounds(pointCloud.bounds);
|
||||||
viewer.render(pointCloud);
|
viewer.render(pointCloud);
|
||||||
// 지도(2D)는 계획노선 기준으로 연다 — 라이다 범위와 다루는 범위가 다르다.
|
// 지도(2D)는 계획노선 기준으로 연다 — 라이다 범위와 다루는 범위가 다르다.
|
||||||
@@ -555,22 +549,21 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
renderInputInfo();
|
renderInputInfo();
|
||||||
updateSelectedModel();
|
updateSelectedModel();
|
||||||
|
|
||||||
// 도엽등고 3D 서피스 — sheet_* 모델이 있으면 별도 컨테이너로 보여준다.
|
// 도엽등고 3D 서피스 — 버튼은 **고를 수 있는 방식 전체**로 세운다. 자동 전처리는
|
||||||
// 보간 방식마다 모델이 하나씩 있으므로 버튼으로 갈아 끼운다.
|
// 기본 하나만 만들므로, 만들어진 모델만 세우면 나머지를 고를 길이 없다.
|
||||||
const sheetMethods = models
|
// 안 만든 방식을 누르면 물어본 뒤 그 방식만 만든다(2026-09-01 사용자 확정).
|
||||||
.filter(
|
const available = await listSheetMethods(projectId).catch(() => null);
|
||||||
(model) =>
|
const builtKeys = new Set(
|
||||||
model.model_type.toLowerCase() === "dtm" &&
|
models
|
||||||
getModelFilter(model).startsWith("sheet_"),
|
.filter(
|
||||||
)
|
(model) =>
|
||||||
.map((model) => ({
|
model.model_type.toLowerCase() === "dtm" && getModelFilter(model).startsWith("sheet_"),
|
||||||
key: getModelFilter(model).slice("sheet_".length),
|
)
|
||||||
label:
|
.map((model) => getModelFilter(model).slice("sheet_".length)),
|
||||||
typeof model.generation_params?.interpolation_label === "string"
|
);
|
||||||
? (model.generation_params.interpolation_label as string)
|
const sheetMethods = (available?.methods ?? [])
|
||||||
: getModelFilter(model).slice("sheet_".length),
|
.map((entry) => ({ key: entry.key, label: entry.label, built: builtKeys.has(entry.key) }))
|
||||||
}))
|
// 목록 순서가 흔들려도 버튼 자리는 정의 순서로 고정한다.
|
||||||
// 모델 목록은 최신순이라 버튼이 뒤섞인다 — 정의 순서로 고정한다.
|
|
||||||
.sort(
|
.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
(SHEET_METHOD_ORDER.indexOf(a.key) + 1 || 99) -
|
(SHEET_METHOD_ORDER.indexOf(a.key) + 1 || 99) -
|
||||||
@@ -585,7 +578,12 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "b04-surface__sheet-method";
|
button.className = "b04-surface__sheet-method";
|
||||||
button.textContent = method.label;
|
button.textContent = method.label;
|
||||||
button.addEventListener("click", () => selectSheetMethod(method.key));
|
// 아직 안 만든 방식은 눌러야 만들어진다 — 눌러 보기 전에 알 수 있게 표시한다.
|
||||||
|
button.classList.toggle("is-unbuilt", !method.built);
|
||||||
|
if (!method.built) button.title = L("B04_Surface_Build_Action");
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
void onSheetMethodClick(method.key, method.label);
|
||||||
|
});
|
||||||
sheetToolbar.append(button);
|
sheetToolbar.append(button);
|
||||||
sheetMethodButtons.set(method.key, button);
|
sheetMethodButtons.set(method.key, button);
|
||||||
}
|
}
|
||||||
@@ -593,10 +591,12 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
sheetViewer.smoothingField.classList.add("b04-surface__sheet-smoothing");
|
sheetViewer.smoothingField.classList.add("b04-surface__sheet-smoothing");
|
||||||
sheetToolbar.append(sheetViewer.smoothingField, lidarLabel);
|
sheetToolbar.append(sheetViewer.smoothingField, lidarLabel);
|
||||||
sheetViewer.setSmoothing(true);
|
sheetViewer.setSmoothing(true);
|
||||||
|
// 처음 열 때 안 만든 방식을 고르면 묻지도 않았는데 모달이 뜬다 — 만들어 둔 것을 고른다.
|
||||||
|
const firstBuilt = sheetMethods.find((method) => method.built);
|
||||||
selectSheetMethod(
|
selectSheetMethod(
|
||||||
sheetMethods.some((method) => method.key === sheetMethod)
|
sheetMethods.some((method) => method.key === sheetMethod && method.built)
|
||||||
? sheetMethod
|
? sheetMethod
|
||||||
: sheetMethods[0].key,
|
: (firstBuilt?.key ?? sheetMethods[0].key),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -625,20 +625,14 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
L("B04_Surface_Confirm_Success")
|
L("B04_Surface_Confirm_Success")
|
||||||
.replace("{filter}", filterGroup.select.value)
|
.replace("{filter}", filterGroup.select.value)
|
||||||
.replace("{method}", methodGroup.select.value)
|
.replace("{method}", methodGroup.select.value)
|
||||||
.replace(
|
.replace("{smoothing}", terrainViewer.isSmoothingEnabled() ? "ON" : "OFF"),
|
||||||
"{smoothing}",
|
|
||||||
terrainViewer.isSmoothingEnabled() ? "ON" : "OFF",
|
|
||||||
),
|
|
||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
await loadProjectData(projectId);
|
await loadProjectData(projectId);
|
||||||
enableRouteStep(projectId);
|
enableRouteStep(projectId);
|
||||||
goToWorkflowStage(projectId, ROUTE_STAGE);
|
goToWorkflowStage(projectId, ROUTE_STAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const detail =
|
const detail = error instanceof Error ? error.message : L("B04_Surface_Confirm_Failed");
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: L("B04_Surface_Confirm_Failed");
|
|
||||||
showToast(`${L("B04_Surface_Confirm_Failed")} ${detail}`, "error");
|
showToast(`${L("B04_Surface_Confirm_Failed")} ${detail}`, "error");
|
||||||
} finally {
|
} finally {
|
||||||
hideLoadingOverlay();
|
hideLoadingOverlay();
|
||||||
@@ -666,8 +660,7 @@ export async function renderB04Surface(root: HTMLElement): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inputSelect.addEventListener("change", () => {
|
inputSelect.addEventListener("change", () => {
|
||||||
selectedInputFile =
|
selectedInputFile = inputFiles.find((file) => String(file.id) === inputSelect.value) ?? null;
|
||||||
inputFiles.find((file) => String(file.id) === inputSelect.value) ?? null;
|
|
||||||
renderInputInfo();
|
renderInputInfo();
|
||||||
});
|
});
|
||||||
filterGroup.select.addEventListener("change", () => {
|
filterGroup.select.addEventListener("change", () => {
|
||||||
|
|||||||
@@ -834,6 +834,12 @@
|
|||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 아직 만들지 않은 방식 — 누르면 계산이 시작된다는 것을 눌러 보기 전에 알린다. */
|
||||||
|
.b04-surface__sheet-method.is-unbuilt:not(.is-active) {
|
||||||
|
color: var(--color-text-muted, var(--color-text-body));
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
.b04-surface__sheet-lidar {
|
.b04-surface__sheet-lidar {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user