/* ============================================================================= * 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(); 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 { 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 }), }; }