자동 체인 상태 전이 분리: - confirm_latest_route/confirm_sections에 mark_stage_complete 플래그 추가 - 자동 체인(신규·재확정)은 데이터만 확정하고 stage 2·3을 IN_PROGRESS로 남김 (스텝바 기존 노란 테두리 = 계산됨·확정 전 상태로 재사용) - B06 [확정]이 stage 2+3 동시 완료 + 경로 상태 CONFIRMED 보강 후 B07 수량 이동 B05 좌측 패널 개편: - 포인트 팔레트 + 임도 기준·옵션을 경로 계산 설정 한 컨테이너로 병합, [최적 경로 계산] 버튼을 컨테이너 내부로 이동 (가끔 쓰는 무거운 재계산) - 하단 액션 행 [초기화][임시저장][횡단 이동]으로 교체, 경로 단독 확정 폐지 - [임시저장] = 관로 + 계획선 델타 + 비정규 측점·상단측 저장(mark_stage_complete=false) - [횡단 이동]/B06 [종단 이동] = 저장 없이 페이지 이동만 (B05·B06 캐시 공유 구조 유지) - [초기화] = POST /route/reset 신설: 기존 경로 삭제 후 계획노선 CSV 기본값으로 자동 체인 재실행(파일입력 직후 상태 복원), 분석용 타임아웃 적용 B06 액션 행: [종단 이동][임시저장][확정] 3버튼 구성, locale 키 정비 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
482 lines
21 KiB
TypeScript
482 lines
21 KiB
TypeScript
import type { PlacedRoutePoint, RoutePointKind } from "./B05_Profile_UI_Markers";
|
|
import {
|
|
createIrregularStationsSection,
|
|
type IrregularStation,
|
|
type IrregularStationsSection,
|
|
} from "./B05_Profile_UI_IrregularStations";
|
|
import { type ButtonVariant, createButton, createSelectField } from "@ui/ui_template_elements";
|
|
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
|
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
|
|
|
|
export interface RoutePanelValues {
|
|
contourInterval: number;
|
|
algorithm: "dijkstra" | "ridge_valley";
|
|
gradeClass: "trunk" | "branch" | "work";
|
|
paved: boolean;
|
|
minCurveRadius: number | null;
|
|
maxUphillGrade: number | null;
|
|
maxDownhillGrade: number | null;
|
|
minUphillGrade: number | null;
|
|
minDownhillGrade: number | null;
|
|
allowAvoidPassThrough: boolean;
|
|
stationInterval: number | null;
|
|
crossSampleInterval: number | null;
|
|
longSampleInterval: number | null;
|
|
terrainType: "normal" | "special";
|
|
maxGradePct: number | null;
|
|
minVerticalRadius: number | null;
|
|
minTangentLength: number | null;
|
|
startElevationOffset: number | null;
|
|
endElevationOffset: number | null;
|
|
}
|
|
|
|
/**
|
|
* 「임도설치 및 관리 등에 관한 규정」[별표 1-2] 기준값.
|
|
* 등급을 바꾸면 이 값이 계획선 폼의 placeholder(미입력 시 서버 기본값)로 반영된다.
|
|
* 실제 기본값 결정은 서버 config가 단일 소스이며 여기서는 안내만 한다.
|
|
*/
|
|
const PROFILE_CRITERIA: Record<
|
|
RoutePanelValues["gradeClass"],
|
|
{ speed: number; grade: Record<RoutePanelValues["terrainType"], number>; radius: number }
|
|
> = {
|
|
trunk: { speed: 40, grade: { normal: 7, special: 10 }, radius: 450 },
|
|
branch: { speed: 30, grade: { normal: 8, special: 12 }, radius: 250 },
|
|
work: { speed: 20, grade: { normal: 9, special: 14 }, radius: 100 },
|
|
};
|
|
|
|
interface PanelCallbacks {
|
|
onSolve: () => void;
|
|
/** [임시저장] — 현재 편집(계획선 델타·관로·비정규 측점·상단측)을 확정 전이 없이 저장. */
|
|
onTempSave: () => void;
|
|
/** [횡단 이동] — 저장 없이 B06 횡단 페이지로 이동만. */
|
|
onGoCross: () => void;
|
|
/** [초기화] — 사용자 편집을 버리고 초기 자동 계산 상태로 롤백. */
|
|
onReset: () => void;
|
|
onContourApply: (interval: number) => void;
|
|
onSurfaceVisible: (visible: boolean) => void;
|
|
onContoursVisible: (visible: boolean) => void;
|
|
onAxesVisible: (visible: boolean) => void;
|
|
onStationLinesVisible: (visible: boolean) => void;
|
|
/** 측점 번호·이름 라벨 표시 토글(기본 켜짐 — BP·EP·5측점 배수·구조물). */
|
|
onStationLabelsVisible: (visible: boolean) => void;
|
|
/** 지표면 흑백 표시 토글(기본 꺼짐 — 무지개 고도색). */
|
|
onSurfaceGrayscale: (grayscale: boolean) => void;
|
|
onView: (view: "iso" | "top" | "front" | "side") => void;
|
|
onResetView: () => void;
|
|
onMovePoint: () => void;
|
|
onDeletePoint: () => void;
|
|
onRadiusChange: (radius: number) => void;
|
|
onInputChange: () => void;
|
|
/** 비정규 측점 목록이 바뀔 때(추가·수정·삭제·리셋). */
|
|
onIrregularChange: (stations: IrregularStation[]) => void;
|
|
/** 비정규 측점을 목록에서 선택/해제할 때 해당 측점(또는 null). */
|
|
onIrregularSelect: (station: IrregularStation | null) => void;
|
|
/** 이어 공사 시작 기준(시작 측점·누가거리 시작)이 바뀔 때. */
|
|
onStationDisplayChange: (offset: { station: number; cumulative: number }) => void;
|
|
}
|
|
|
|
type WrappedInput = HTMLInputElement & { wrapper: HTMLLabelElement };
|
|
|
|
function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
function section(title: string): { root: HTMLElement; body: HTMLElement } {
|
|
const root = document.createElement("section");
|
|
// ui-sidebar-section: 사이드 컨테이너 공통 외곽선(진하게, 2026-08-05 사용자 지시).
|
|
root.className = "b05-route__panel-section ui-collapsible ui-sidebar-section";
|
|
const heading = document.createElement("h3");
|
|
heading.className = "ui-collapsible__title";
|
|
heading.textContent = title;
|
|
const body = document.createElement("div");
|
|
body.className = "b05-route__panel-body";
|
|
root.append(heading, body);
|
|
return { root, body };
|
|
}
|
|
|
|
function button(
|
|
label: string,
|
|
onClick: () => void,
|
|
variant: ButtonVariant = "ghost",
|
|
): HTMLButtonElement {
|
|
return createButton({ label, variant, onClick: () => onClick() });
|
|
}
|
|
|
|
function numberField(label: string, value = ""): WrappedInput {
|
|
const wrapper = document.createElement("label");
|
|
wrapper.className = "b05-route__field";
|
|
const caption = document.createElement("span");
|
|
caption.textContent = label;
|
|
const input = document.createElement("input");
|
|
input.type = "number";
|
|
input.step = "0.01";
|
|
input.value = value;
|
|
wrapper.append(caption, input);
|
|
return Object.assign(input, { wrapper });
|
|
}
|
|
|
|
function checkbox(label: string, checked: boolean): WrappedInput {
|
|
const wrapper = document.createElement("label");
|
|
wrapper.className = "b05-route__check";
|
|
const input = document.createElement("input");
|
|
input.type = "checkbox";
|
|
input.checked = checked;
|
|
wrapper.append(input, document.createTextNode(label));
|
|
return Object.assign(input, { wrapper });
|
|
}
|
|
|
|
function toggleButton(
|
|
label: string,
|
|
checked: boolean,
|
|
onChange: (checked: boolean) => void,
|
|
): HTMLButtonElement {
|
|
const element = button(
|
|
label,
|
|
() => {
|
|
const active = !element.classList.contains("is-active");
|
|
element.classList.toggle("is-active", active);
|
|
element.setAttribute("aria-pressed", String(active));
|
|
onChange(active);
|
|
},
|
|
"glass",
|
|
);
|
|
element.classList.toggle("is-active", checked);
|
|
element.setAttribute("aria-pressed", String(checked));
|
|
return element;
|
|
}
|
|
|
|
function parseOptional(input: HTMLInputElement): number | null {
|
|
if (!input.value.trim()) return null;
|
|
const value = Number(input.value);
|
|
return Number.isFinite(value) ? value : null;
|
|
}
|
|
|
|
export function createRoutePanel(callbacks: PanelCallbacks) {
|
|
const root = document.createElement("div");
|
|
root.className = "b05-route__panel";
|
|
|
|
const viewControls = document.createElement("div");
|
|
viewControls.className = "b05-route__view-controls";
|
|
const viewButtons = document.createElement("div");
|
|
viewButtons.className = "b05-route__view-group";
|
|
(["iso", "top", "front", "side"] as const).forEach((preset) =>
|
|
viewButtons.append(button(preset.toUpperCase(), () => callbacks.onView(preset), "glass")),
|
|
);
|
|
const visibilityButtons = document.createElement("div");
|
|
visibilityButtons.className = "b05-route__view-group";
|
|
visibilityButtons.append(
|
|
toggleButton("지표면", true, callbacks.onSurfaceVisible),
|
|
toggleButton("등고선", true, callbacks.onContoursVisible),
|
|
toggleButton("축 표시", false, callbacks.onAxesVisible),
|
|
toggleButton(L("B05_Route_Field_StationLines"), true, callbacks.onStationLinesVisible),
|
|
toggleButton(L("B05_Route_Field_StationLabels"), true, callbacks.onStationLabelsVisible),
|
|
// 무지개 고도색이 헷갈릴 때 명도만 남기는 흑백 표시(2026-08-05 사용자 요청).
|
|
toggleButton("흑백 지형", false, callbacks.onSurfaceGrayscale),
|
|
);
|
|
const separator1 = document.createElement("span");
|
|
separator1.className = "b05-route__view-separator";
|
|
const separator2 = separator1.cloneNode() as HTMLSpanElement;
|
|
viewControls.append(
|
|
viewButtons,
|
|
separator1,
|
|
visibilityButtons,
|
|
separator2,
|
|
button("뷰 초기화", callbacks.onResetView, "glass"),
|
|
);
|
|
|
|
const contour = section("등고선 간격");
|
|
contour.root.classList.add("is-collapsed");
|
|
const contourInterval = numberField("간격 (m), 최소 0.5m", "1");
|
|
const contourRow = document.createElement("div");
|
|
contourRow.className = "b05-route__contour-row";
|
|
contourRow.append(
|
|
contourInterval.wrapper,
|
|
button("재적용", () => callbacks.onContourApply(Number(contourInterval.value) || 1)),
|
|
);
|
|
contour.body.append(contourRow);
|
|
|
|
// 포인트 팔레트 + 임도 기준·옵션을 한 컨테이너로 병합하고 [최적 경로 계산]도 이 안에
|
|
// 둔다 — 경로 재탐색은 B04~B06 재계산을 부르는 무거운 작업이라 가끔만 쓴다
|
|
// (2026-08-08 사용자 지시).
|
|
const routeCalc = section("경로 계산 설정");
|
|
routeCalc.root.classList.add("is-collapsed");
|
|
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);
|
|
});
|
|
routeCalc.body.append(paletteGrid);
|
|
|
|
const selected = section("선택 포인트 상세 설정");
|
|
selected.root.hidden = true;
|
|
const selectedName = document.createElement("strong");
|
|
const radius = numberField("회피/금지 반경 (m)", "25");
|
|
radius.addEventListener("change", () => callbacks.onRadiusChange(Number(radius.value) || 1));
|
|
const selectedActions = document.createElement("div");
|
|
selectedActions.className = "b05-route__actions";
|
|
selectedActions.append(
|
|
button("위치 이동", callbacks.onMovePoint),
|
|
button("삭제", callbacks.onDeletePoint, "danger"),
|
|
);
|
|
selected.body.append(selectedName, radius.wrapper, selectedActions);
|
|
|
|
// 드롭다운은 공통 컴포넌트(createSelectField) 재사용. `.select`로 받아 이하 로직 불변.
|
|
const algorithmField = createSelectField({
|
|
label: "알고리즘",
|
|
options: [
|
|
{ value: "dijkstra", text: "Dijkstra" },
|
|
{ value: "ridge_valley", text: "능선·계곡" },
|
|
],
|
|
});
|
|
const algorithm = algorithmField.select;
|
|
const gradeField = createSelectField({
|
|
label: "임도 등급",
|
|
options: [
|
|
{ value: "trunk", text: "간선" },
|
|
{ value: "branch", text: "지선" },
|
|
{ value: "work", text: "작업" },
|
|
],
|
|
});
|
|
const gradeClass = gradeField.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,
|
|
);
|
|
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
|
routeCalc.body.append(
|
|
algorithmField.root,
|
|
gradeField.root,
|
|
paved.wrapper,
|
|
avoidPass.wrapper,
|
|
details,
|
|
solveButton,
|
|
);
|
|
|
|
const sectionOptions = section(L("B05_Route_Group_SectionOptions"));
|
|
sectionOptions.root.classList.add("is-collapsed");
|
|
const stationInterval = numberField(L("B05_Route_Field_StationInterval"));
|
|
const crossSampleInterval = numberField(L("B05_Route_Field_CrossSample"));
|
|
const longSampleInterval = numberField(L("B05_Route_Field_LongSample"));
|
|
sectionOptions.body.append(
|
|
stationInterval.wrapper,
|
|
crossSampleInterval.wrapper,
|
|
longSampleInterval.wrapper,
|
|
);
|
|
|
|
const gradeLine = section("종단 설계 기준");
|
|
const terrainField = createSelectField({
|
|
label: "지형 구분",
|
|
options: [
|
|
{ value: "normal", text: "일반지형" },
|
|
{ value: "special", text: "특수지형" },
|
|
],
|
|
});
|
|
const terrainType = terrainField.select;
|
|
// 역기울기(5%) 상한 방향은 서버가 지반 형상에서 자동 판정(main_direction="auto")하므로
|
|
// 수동 선택 UI는 두지 않는다. 노선 균형 구역 길이도 자동 산출 기본값(전체 1구역)에 맡긴다.
|
|
const maxGradePct = numberField("최대 종단기울기 (%)");
|
|
const minVerticalRadius = numberField("종단곡선 최소 반경 (m)");
|
|
const minTangentLength = numberField("최소 직선 길이 (m)");
|
|
const startElevationOffset = numberField("시점 계획고 조정 (m)");
|
|
const endElevationOffset = numberField("종점 계획고 조정 (m)");
|
|
const criteriaNote = document.createElement("p");
|
|
criteriaNote.className = "b05-route__note";
|
|
const gradeAdvanced = document.createElement("details");
|
|
const gradeSummary = document.createElement("summary");
|
|
gradeSummary.textContent = "기준값 직접 지정";
|
|
gradeAdvanced.append(
|
|
gradeSummary,
|
|
maxGradePct.wrapper,
|
|
minVerticalRadius.wrapper,
|
|
minTangentLength.wrapper,
|
|
startElevationOffset.wrapper,
|
|
endElevationOffset.wrapper,
|
|
);
|
|
gradeLine.body.append(terrainField.root, criteriaNote, gradeAdvanced);
|
|
|
|
// 공사 시작 기준 — 이전 공사에 이어 시공할 때 0측점을 임의 측점/누가거리로 시작 표기한다.
|
|
// (내부 chainage는 0기준 유지, 측점 라벨·누가거리 "표시"만 이 값만큼 이동.) 기본값 0/0.
|
|
// 별도 "공사 시작점" 컨테이너를 없애고 "시작 측점 및 샘플링 설정" 최상단으로 일원화
|
|
// (2026-08-06 사용자 지시).
|
|
const startStation = numberField("시작 측점", "0");
|
|
startStation.step = "1";
|
|
startStation.min = "0";
|
|
const startCumulative = numberField("시작 누가거리 (m)", "0");
|
|
// 2열×2행: 1행 항목명 · 2행 값 입력 / 1열 시작 측점 · 2열 시작 누가거리.
|
|
const startRow = document.createElement("div");
|
|
startRow.className = "b05-route__field-row";
|
|
startRow.append(startStation.wrapper, startCumulative.wrapper);
|
|
sectionOptions.body.prepend(startRow);
|
|
const stationDisplayOffset = (): { station: number; cumulative: number } => ({
|
|
station: Math.max(0, Math.round(Number(startStation.value) || 0)),
|
|
cumulative: Number(startCumulative.value) || 0,
|
|
});
|
|
[startStation, startCumulative].forEach((input) =>
|
|
input.addEventListener("change", () =>
|
|
callbacks.onStationDisplayChange(stationDisplayOffset()),
|
|
),
|
|
);
|
|
|
|
// 비정규 측점(구조물 측점) — 측점번호+잔여거리로 추가/수정/삭제. 목록 변경은 Page로 올려
|
|
// 그래프·테이블·3D에 반영한다. chainage 환산 기준인 측점간격은 실시간 조회한다.
|
|
const irregular = createIrregularStationsSection({
|
|
getInterval: () => Number(stationInterval.value) || 20,
|
|
onChange: callbacks.onIrregularChange,
|
|
onSelect: callbacks.onIrregularSelect,
|
|
});
|
|
|
|
/** 등급·지형 선택에 맞춰 법정 기준값을 placeholder와 안내문에 반영한다. */
|
|
function syncCriteria(): void {
|
|
const criteria = PROFILE_CRITERIA[gradeClass.value as RoutePanelValues["gradeClass"]];
|
|
const terrain = terrainType.value as RoutePanelValues["terrainType"];
|
|
maxGradePct.placeholder = String(criteria.grade[terrain]);
|
|
minVerticalRadius.placeholder = String(criteria.radius);
|
|
minTangentLength.placeholder = "20";
|
|
criteriaNote.textContent =
|
|
`설계속도 ${criteria.speed}km/h 기준 — 종단기울기 ${criteria.grade[terrain]}% 이하, ` +
|
|
`종단곡선 반경 ${criteria.radius}m 이상. 비워두면 이 기준이 적용됩니다.`;
|
|
}
|
|
gradeClass.addEventListener("change", syncCriteria);
|
|
terrainType.addEventListener("change", syncCriteria);
|
|
syncCriteria();
|
|
|
|
// 하단 고정 액션 행: [초기화][임시저장][횡단 이동] — 경로 확정 개념 폐지, 종·횡 통합
|
|
// 확정은 B06에서 한다(2026-08-08 워크플로우 재정의).
|
|
const resetButton = button(L("Common_Btn_Reset"), callbacks.onReset, "danger");
|
|
const tempSaveButton = button(L("B05_Route_Btn_TempSave"), callbacks.onTempSave);
|
|
const goCrossButton = button(L("B05_Route_Btn_GoCross"), callbacks.onGoCross, "filled");
|
|
const actionRow = document.createElement("div");
|
|
// 사이드 최하단 고정(공용 ui-sidebar-actions) — 스크롤에서 제외(2026-08-05 사용자 지시).
|
|
actionRow.className = "b05-route__actions ui-sidebar-actions";
|
|
actionRow.append(resetButton, tempSaveButton, goCrossButton);
|
|
|
|
const inputElements = [
|
|
algorithm,
|
|
gradeClass,
|
|
paved,
|
|
avoidPass,
|
|
minCurveRadius,
|
|
maxUphillGrade,
|
|
maxDownhillGrade,
|
|
minUphillGrade,
|
|
minDownhillGrade,
|
|
stationInterval,
|
|
crossSampleInterval,
|
|
longSampleInterval,
|
|
terrainType,
|
|
maxGradePct,
|
|
minVerticalRadius,
|
|
minTangentLength,
|
|
startElevationOffset,
|
|
endElevationOffset,
|
|
];
|
|
inputElements.forEach((input) => input.addEventListener("change", callbacks.onInputChange));
|
|
root.append(
|
|
gradeLine.root,
|
|
contour.root,
|
|
sectionOptions.root,
|
|
irregular.root,
|
|
routeCalc.root,
|
|
selected.root,
|
|
actionRow,
|
|
);
|
|
|
|
// 컨테이너 제목 행 전체 클릭 시 본문을 접거나 편다(공용 collapsible). 내부 details 등 별도
|
|
// 접힘 항목은 손대지 않는다.
|
|
attachCollapsible(root);
|
|
|
|
return {
|
|
root,
|
|
viewControls,
|
|
/** 비정규 측점 섹션 API(목록 조회·선택·초기화). 아직 백엔드로 보내지 않는다(프론트 프리뷰). */
|
|
irregularStations: irregular as IrregularStationsSection,
|
|
/** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */
|
|
stationDisplayOffset,
|
|
values(): RoutePanelValues {
|
|
return {
|
|
contourInterval: Number(contourInterval.value) || 1,
|
|
algorithm: algorithm.value as RoutePanelValues["algorithm"],
|
|
gradeClass: gradeClass.value as RoutePanelValues["gradeClass"],
|
|
paved: paved.checked,
|
|
minCurveRadius: parseOptional(minCurveRadius),
|
|
maxUphillGrade: parseOptional(maxUphillGrade),
|
|
maxDownhillGrade: parseOptional(maxDownhillGrade),
|
|
minUphillGrade: parseOptional(minUphillGrade),
|
|
minDownhillGrade: parseOptional(minDownhillGrade),
|
|
allowAvoidPassThrough: avoidPass.checked,
|
|
stationInterval: parseOptional(stationInterval),
|
|
crossSampleInterval: parseOptional(crossSampleInterval),
|
|
longSampleInterval: parseOptional(longSampleInterval),
|
|
terrainType: terrainType.value as RoutePanelValues["terrainType"],
|
|
maxGradePct: parseOptional(maxGradePct),
|
|
minVerticalRadius: parseOptional(minVerticalRadius),
|
|
minTangentLength: parseOptional(minTangentLength),
|
|
startElevationOffset: parseOptional(startElevationOffset),
|
|
endElevationOffset: parseOptional(endElevationOffset),
|
|
};
|
|
},
|
|
restore(values: Partial<RoutePanelValues>) {
|
|
if (values.contourInterval != null) contourInterval.value = String(values.contourInterval);
|
|
if (values.algorithm) algorithm.value = values.algorithm;
|
|
if (values.gradeClass) gradeClass.value = values.gradeClass;
|
|
if (values.paved != null) paved.checked = values.paved;
|
|
if (values.minCurveRadius != null) minCurveRadius.value = String(values.minCurveRadius);
|
|
if (values.maxUphillGrade != null) maxUphillGrade.value = String(values.maxUphillGrade);
|
|
if (values.maxDownhillGrade != null) maxDownhillGrade.value = String(values.maxDownhillGrade);
|
|
if (values.minUphillGrade != null) minUphillGrade.value = String(values.minUphillGrade);
|
|
if (values.minDownhillGrade != null) minDownhillGrade.value = String(values.minDownhillGrade);
|
|
if (values.allowAvoidPassThrough != null) avoidPass.checked = values.allowAvoidPassThrough;
|
|
if (values.stationInterval != null) stationInterval.value = String(values.stationInterval);
|
|
if (values.crossSampleInterval != null)
|
|
crossSampleInterval.value = String(values.crossSampleInterval);
|
|
if (values.longSampleInterval != null)
|
|
longSampleInterval.value = String(values.longSampleInterval);
|
|
if (values.terrainType) terrainType.value = values.terrainType;
|
|
if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct);
|
|
if (values.minVerticalRadius != null)
|
|
minVerticalRadius.value = String(values.minVerticalRadius);
|
|
if (values.minTangentLength != null) minTangentLength.value = String(values.minTangentLength);
|
|
if (values.startElevationOffset != null)
|
|
startElevationOffset.value = String(values.startElevationOffset);
|
|
if (values.endElevationOffset != null)
|
|
endElevationOffset.value = String(values.endElevationOffset);
|
|
syncCriteria();
|
|
},
|
|
setSelected(point: PlacedRoutePoint | null) {
|
|
selected.root.hidden = !point;
|
|
if (!point) return;
|
|
selectedName.textContent = `${point.type.toUpperCase()} (${point.x.toFixed(2)}, ${point.y.toFixed(2)})`;
|
|
radius.wrapper.hidden = point.type !== "ap" && point.type !== "fp";
|
|
radius.value = String(point.radius_m ?? 25);
|
|
},
|
|
};
|
|
}
|
|
|
|
export type RoutePanel = ReturnType<typeof createRoutePanel>;
|