Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_RecPreview.ts
T
eomsangdonandClaude Opus 5.5 99ec4b8efe feat(B05 평면 지도): 공용 도구줄 표 · 겹 줄 · 범례 · 지형 칩 붙임 · 노선추천 = 도구줄 미리보기(넷 · 하나만 · 끔) · [적용] · 추천 모달 · 좌측 칸 걷음 · 지형 바꿈 = 지형 칩 (PLAN 624-3 · 626-1 · 요구 530 · 531)
- 624-3 도구줄 = 공용 표 「b05」 · 아래 줄 = 공용 겹 줄(띠 · 올린 임도망도 숨김 · 처음 = 등고선 · 하천) · 범례 = 공용 legendRows(상태 글판 위)
- 노선 띠 300 m 자르기 = B05 자기 칸(등고선 · 하천 · 1 m)만 · 그 밖 겹 = 안 자르는 둘째 GL 칸(맨 밑 · 켠 겹 있을 때만 · GL 만) · 등고선 끔 = 등고 글도 숨김
- 626-1 추천 보기 = 도구줄 아이콘 + 아래 판(영선 · 토공 최소 · 기울기 최소 · 비중 계산 · 끔) — route_recommend 저장본(지금 설계 지형) 선만 · 다시 셈 · 쓰기 없음
- 지형 칩 → 확인창 → 새 POST /route/design-terrain(설계 지형만 바꿈 · 노선 저장 체인 다시 · 노선 · 초기값 · 추천 저장본 그대로 · 잠금 · 실패 되돌림)
- 걷음 — 추천 모달(_Recommend · _Help · _Map · CSS) · 좌측 「노선 추천」 칸 · saveRecommendedRoute · UI_RouteReset(서버 as_initial 길 · 시험은 남김)
- 부품 상자 — createPickMenu(아이콘 고르기 메뉴 · 판 = body fixed · ✓)
- 낡은 시험 고침 · 지움 — 모달 · 좌측 칸 · [적용] 화면 배선을 보던 시험(283 · 346_3 · 349 · 364_4 · 383_1 · 390_5 · 418 · 421 · 421_2 · 476_2 · 509_2 · 509_5 · 551_2 · 600_3 · b05_routeedit_initial · design_view_parity · modal_top · 133_1 · 133_2) · test_508_2 · helper_283 지움(모달 전용)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:30:49 +09:00

106 lines
4.3 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_RouteEdit_RecPreview.ts
* 평면 노선 추천 미리보기(PLAN 626-1 · 요구 530 · 결정 D1 · D7 · D8) — 평면 도구줄 아이콘 하나 + 아래 판
* (영선 · 토공 최소 · 기울기 최소 · 비중 계산 · 끔) · 하나만 보임 · 선만(위 캔버스 · 노선 아래 그림 · 집기 없음).
* 선 = 노선 추천 저장본(`B05_Profile/route_recommend/` · 지금 설계 지형 · 체인이 미리 셈) 그대로 — 다시 셈 · 쓰기 없음 ·
* 저장본 없으면(409) 알림 · 끔. 옛 추천 모달 · 좌측 「노선 추천」 칸 · [적용](`as_initial`) 은 걷음(서버 길 · 시험은 남김).
* 부르는 곳 = `_Chrome`(도구줄 `recommend` 칸) · `_RouteEdit`(그리기 `overStations` 앞).
* ========================================================================== */
import { createPickMenu } from "@ui/ui_template_context_menu";
import { showToast } from "@ui/ui_template_elements";
import { themeColor } from "@ui/ui_template_palette";
import type { AnalysisTerrain } from "./B05_Profile_Api_Replan";
import { fetchRouteInitial, type RouteInitialLogic } from "./B05_Profile_Api_RouteInitial";
import { LOGIC_LABEL } from "./B05_Profile_UI_RouteEdit_Initial_Model";
type Vertex = [number, number];
/** 드롭다운 넷(요구 530 · 「예상노선 추적」 = 초기값이라 뺌). */
const MODES = [
"gradeline",
"earthwork",
"gentle",
"weighted",
] as const satisfies readonly RouteInitialLogic[];
export interface RecPreview {
root: HTMLElement;
/** 노선 그리기 안(돌림 안) — 고른 선 하나. */
draw: (context: CanvasRenderingContext2D, toScreen: (vertex: Vertex) => [number, number]) => void;
/** 검증 창구 — 고른 갈래 · 선 점 수. */
state: () => { mode: string | null; points: number };
}
export function createRecPreview(opts: {
projectId: string;
/** 지금 설계 지형(저장본 고름 · D7). */
terrain: () => AnalysisTerrain;
redraw: () => void;
}): RecPreview {
const lines = new Map<string, Vertex[]>();
let shown: Vertex[] | null = null;
let turn = 0;
const menu = createPickMenu({
icon: "compass",
title: "추천 노선 보기 — 노선 추천 저장본 선 하나만 겹쳐 봄(끔 = 안 보임 · 노선은 안 바뀜)",
items: MODES.map((mode) => ({ value: mode, label: LOGIC_LABEL[mode] })),
offLabel: "끔",
onPick: (mode) => void pick(mode as RouteInitialLogic | null),
});
menu.root.dataset.act = "recommend";
async function pick(mode: RouteInitialLogic | null): Promise<void> {
const mine = ++turn;
shown = null;
opts.redraw();
if (!mode) return;
const key = `${mode}|${opts.terrain()}`;
try {
if (!lines.has(key)) {
const body = (await fetchRouteInitial(opts.projectId, {
mode,
with_profile: true,
terrain: opts.terrain(),
})) as { planned?: number[][] };
const planned = (body.planned ?? []).map(([x, y]) => [x, y] as Vertex);
if (planned.length < 2) throw new Error("추천 선이 비었음");
lines.set(key, planned);
}
if (mine !== turn) return;
shown = lines.get(key)!;
opts.redraw();
} catch (error) {
if (mine !== turn) return;
menu.set(null);
const text = error instanceof Error ? error.message : String(error);
showToast(`추천 노선 「${LOGIC_LABEL[mode]}」 못 봄 — ${text}`, "info");
}
}
return {
root: menu.root,
draw(context, toScreen) {
if (!shown) return;
context.save();
context.setLineDash([10, 6]);
context.lineJoin = "round";
context.beginPath();
shown.forEach((vertex, index) => {
const [x, y] = toScreen(vertex);
if (index) context.lineTo(x, y);
else context.moveTo(x, y);
});
// 지도 위 그림 색(테마 안 따름) + 검은 테 — 위성 · 어두운 바탕 모두 보임
context.strokeStyle = "rgba(0, 0, 0, 0.6)";
context.lineWidth = 5;
context.stroke();
context.strokeStyle = themeColor("--draw-map-sketch", "#ffe94d");
context.lineWidth = 3;
context.stroke();
context.restore();
},
state: () => ({ mode: menu.value(), points: shown?.length ?? 0 }),
};
}