/* Old — 54-24 · 나중에 삭제 */ /* ============================================================================= * B05_Profile_UI_RouteCalc.ts * 좌측 패널 「경로 계산 설정」 칸 — 포인트 팔레트 · 알고리즘 · 조건 · [최적 경로 계산](숨김). * `B05_Profile_UI_Panel_Route.ts` · `_Panel_Sections.ts` · `_Page_Actions.ts` · `_Page_Helpers.ts` * 에서 떼어 옮겼다. 어디서도 import 하지 않는다(tsconfig 제외). * ========================================================================== */ import { createSelectField, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { createSectionFrame, part, registerPanelSection, } from "../../A00_Common/design/design_panel"; import { solveRoute, type CirclePoint, type RoutePoint } from "../B05_Profile_Api_Fetch"; import type { PlacedRoutePoint, RoutePointKind } from "../B05_Profile_UI_Markers"; import { button, checkbox, numberField, parseOptional } from "../B05_Profile_UI_Panel_Fields"; import type { PageActionContext } from "../B05_Profile_UI_Page_Actions"; export function createRouteCalcPart(callbacks: { onSolve: () => void }) { const paletteGrid = document.createElement("div"); paletteGrid.className = "b05-route__palette"; const pointLabels: Record = { bp: "BP 시작점", ep: "EP 종료점", cp: "CP 경유점", ap: "AP 회피구역", fp: "FP 금지구역", }; (Object.keys(pointLabels) as RoutePointKind[]).forEach((kind) => { const chip = document.createElement("div"); chip.className = `b05-route__chip is-${kind}`; chip.draggable = true; chip.textContent = pointLabels[kind]; chip.addEventListener("dragstart", (event) => event.dataTransfer?.setData("pointType", kind)); paletteGrid.append(chip); }); // 드롭다운은 공통 컴포넌트(createSelectField) 재사용. `.select`로 받아 이하 로직 불변. const algorithmField = createSelectField({ label: "알고리즘", options: [ { value: "dijkstra", text: "Dijkstra" }, { value: "ridge_valley", text: "능선·계곡" }, ], }); const algorithm = algorithmField.select; const minCurveRadius = numberField("최소 곡선반경 (m)"); const maxUphillGrade = numberField("오르막 경사 상한 (%)"); const maxDownhillGrade = numberField("내리막 경사 상한 (%)"); const minUphillGrade = numberField("오르막 경사 하한 (%)"); const minDownhillGrade = numberField("내리막 경사 하한 (%)"); const paved = checkbox("포장 임도", false); const avoidPass = checkbox("회피구역 통과 허용", false); const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "사용한 조건"; details.append( summary, minCurveRadius.wrapper, maxUphillGrade.wrapper, maxDownhillGrade.wrapper, minUphillGrade.wrapper, minDownhillGrade.wrapper, ); // [최적 경로 계산] 입구는 2026-09-06 사용자 확정으로 화면에서 내렸다 — 평면만 보고 // 노선을 정하는 방식이라 실제 판단(평면·종단·횡단·유토곡선을 함께 봄)과 맞지 않는다. const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled"); solveButton.hidden = true; return { /** 「경로 계산 설정」 본문 — 화면 순서대로. */ calcNodes: [ paletteGrid, algorithmField.root, paved.wrapper, avoidPass.wrapper, details, solveButton, ] as HTMLElement[], fields: { algorithm, paved, avoidPass, minCurveRadius, maxUphillGrade, maxDownhillGrade, minUphillGrade, minDownhillGrade, }, }; } export type RouteCalcPart = ReturnType; registerPanelSection({ id: "route-calc", title: "경로 계산 설정", views: ["profile"], order: 40, build: (ctx, view) => { const frame = createSectionFrame(view, "경로 계산 설정", true); frame.body.append(...part(ctx, "routeCalc").calcNodes); // 경로 계산 설정 — 사용여부 미결정으로 조작 불가(숨기지 않는다, 2026-08-18 사용자 // 지시). inert가 본문 전체 입력·드래그를 막고, is-disabled가 흐림을 그린다. // 제목 클릭(접기/펼치기)은 본문 밖이라 그대로 산다. frame.root.classList.add("is-disabled"); frame.body.inert = true; return frame.root; }, }); export function routePoint(point: PlacedRoutePoint): RoutePoint { // 서버 스키마는 표고를 빼면 "모름"으로 받는다 — null은 undefined로 바꿔 보낸다. return { x: point.x, y: point.y, z: point.z ?? undefined }; } export function circlePoint(point: PlacedRoutePoint): CirclePoint { return { ...routePoint(point), radius_m: point.radius_m ?? 25 }; } /** [경로 계산] — 마커·패널 값으로 노선을 풀고 종횡단까지 다시 받는다. */ export async function solveRouteAction( ctx: PageActionContext, calc: RouteCalcPart["fields"], ): Promise { const latest = ctx.latest(); const confirmedSurface = ctx.confirmedSurface(); if (!confirmedSurface || !latest) return; const points = ctx.viewer().markers.getPoints(); if (!points.bp || !points.ep) { showToast("BP와 EP를 지형에 배치하세요.", "error"); return; } const values = ctx.panel().values(); showLoadingOverlay(); try { const solved = await solveRoute(ctx.projectId, { filter_key: latest.surface_params.source_filter, method: latest.surface_params.method, smooth: latest.surface_params.smooth, surface_model_id: confirmedSurface.model_id ?? undefined, algorithm: calc.algorithm.value, bp: routePoint(points.bp), ep: routePoint(points.ep), cp: points.cp.map(routePoint), ap: points.ap.map(circlePoint), fp: points.fp.map(circlePoint), grade_class: values.gradeClass, paved: calc.paved.checked, min_curve_radius_m: parseOptional(calc.minCurveRadius), max_uphill_grade: parseOptional(calc.maxUphillGrade), max_downhill_grade: parseOptional(calc.maxDownhillGrade), min_uphill_grade: parseOptional(calc.minUphillGrade), min_downhill_grade: parseOptional(calc.minDownhillGrade), allow_avoid_pass_through: calc.avoidPass.checked, station_interval_m: values.stationInterval, cross_half_width_m: null, cross_sample_interval_m: values.crossSampleInterval, long_sample_interval_m: values.longSampleInterval, terrain_type: values.terrainType, design_speed_kph: values.designSpeed, max_grade_pct: values.maxGradePct, min_vertical_radius_m: values.minVerticalRadius, min_tangent_length_m: values.minTangentLength, start_elevation_offset_m: values.startElevationOffset, end_elevation_offset_m: values.endElevationOffset, enforce_pipe_clearance: values.enforcePipeClearance, }); // 새 경로는 측점 구성이 달라지므로 이전 상단측 변경분을 폐기한다(자동 판정 재사용). ctx.uphillOverrides.clear(); ctx.persistUphillOverrides(); ctx.renderLatest(await ctx.loadLatest(true)); await ctx.restoreSections(solved.route_id); if (solved.cross_section_count === null) { showToast("경로는 저장되었지만 종횡단 생성에 실패했습니다.", "error"); } else if (solved.grade_summary === null) { showToast("경로·종횡단은 저장되었지만 계획선 산출에 실패했습니다.", "error"); } else { showToast("최적 경로 계산이 완료되었습니다.", "success"); } } catch (error) { showToast(error instanceof Error ? error.message : "경로 계산에 실패했습니다.", "error"); } finally { hideLoadingOverlay(); } }