- 상자 화면(포인트 팔레트 · 알고리즘 · 조건 · 숨긴 [최적 경로 계산]) · solveRouteAction · routePoint/circlePoint 를 B05_Profile/Old/B05_Profile_UI_RouteCalc.ts 로 떼어 옮김 - 좌측 패널 등록 · onSolve · 칸 값(algorithm · paved · 곡선반경 · 경사 상하한 · 회피 통과) 끊음 - tsconfig 에서 B05_Profile/Old 제외 · 서버 경로 계산 API · 자동설계 체인은 그대로 - 시험: 좌측 칸 수 1+4+2 · 칸 제목에서 「경로 계산 설정」 뺌 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4uKaV4ZBhsJN4GHG7yRj4
193 lines
7.7 KiB
TypeScript
193 lines
7.7 KiB
TypeScript
/* 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<RoutePointKind, string> = {
|
|
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<typeof createRouteCalcPart>;
|
|
|
|
registerPanelSection({
|
|
id: "route-calc",
|
|
title: "경로 계산 설정",
|
|
views: ["profile"],
|
|
order: 40,
|
|
build: (ctx, view) => {
|
|
const frame = createSectionFrame(view, "경로 계산 설정", true);
|
|
frame.body.append(...part<RouteCalcPart>(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<void> {
|
|
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();
|
|
}
|
|
}
|