/* ============================================================================= * D03_PlanAnalysis_UI_Props.ts * D03 고른 구간 속성 판 · 좌측 「구역 · 연도」 목록(PLAN 585-2 · 요구서 445 A4) — 오토캐드 특성 창 꼴. * * 속성 판 = 지도 위 떠 있는 판(공용 `createFloatPanel` · 머리 끌어 옮김 · 오른쪽 위) · 고름 있을 때만 — * 머리 「구간 N · 선 M · L m」 · ✕ = 풂 · 넓이 [구간][선][같은 해] · 연도(모두 같으면 그 해 · 다르면 「여러 해」 → * 고르면 고른 구간 모두 그 해 = 되돌리기 한 걸음) · 고른 구간 줄(누름 = 그 구간만) · * [방향 바꾸기] [갈래 시점 지정] [선 지움](선 하나일 때) · [마크 지움](마크 고름) · 보기만 계정 = 값만. * 측점 방향(585-3 · 요구 446 3) = 사용자가 정함 — 본선 = [방향 바꾸기] · 다른 선에 닿은 끝 = [갈래 시점 지정] * (그 점이 시점 · 꼬리에 닿은 머리 = 한 선으로 이음) · 자동 판정 없음. * 좌측 목록 = 보기 + 고르기만(해 고침은 속성 판 한 곳) — 해 줄(같은 해) · 선 머리 줄(선) · 구간 줄(구간). * ========================================================================== */ import { createButton } from "@ui/ui_template_elements"; import { createFloatPanel } from "@ui/ui_template_float_panel"; import { createSelectField } from "@ui/ui_template_input"; import { createSegment } from "@ui/ui_template_segment"; import { createText } from "@ui/ui_template_text"; import { stationLabel } from "../common_util/common_util_station"; import { lineLength } from "./logic/D03_PlanAnalysis_Draw"; import { lineNames, setPiecesYear, type PlanNetwork } from "./logic/D03_PlanAnalysis_Plan"; import type { PlanEdit, PlanScope } from "./D03_PlanAnalysis_UI_Edit"; import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map"; /** 조각 연도 고를 수 — 사업연도 시작부터. */ const YEAR_SPAN = 10; const station = (m: number): string => stationLabel(m, PLAN_STATION_INTERVAL_M, 1); const meters = (m: number): string => `${m.toFixed(1)} m`; export interface PlanPropsParams { network: () => PlanNetwork; edit: PlanEdit; editable: () => boolean; yearStart: () => number; commit: (next: PlanNetwork) => void; } export interface PlanProps { /** 지도 위 속성 판(지도 칸에 붙임). */ panel: HTMLElement; /** 좌측 「구역 · 연도」 목록 몸. */ list: HTMLElement; /** 고름 · 꼴이 바뀜 — 판 · 목록 다시. */ render: () => void; /** 고를 해 — 사업연도 시작부터 열 해 + 조각에 있는 해. */ yearOptions: () => Array<{ value: string; text: string }>; } export function createPlanProps(params: PlanPropsParams): PlanProps { const { edit } = params; const float = createFloatPanel({ title: "고른 구간", draggable: true, onClose: () => edit.select([]), }); float.root.classList.add("d03-plan__props"); float.root.dataset.role = "plan-props"; float.root.hidden = true; const list = document.createElement("div"); list.className = "d03-plan__pieces"; function yearOptions(): Array<{ value: string; text: string }> { const start = params.yearStart(); const years = new Set(Array.from({ length: YEAR_SPAN }, (_, i) => start + i)); params.network().pieces.forEach((piece) => years.add(piece.year)); return [...years].sort().map((year) => ({ value: String(year), text: `${year}년` })); } function renderPanel(): void { const picked = edit.selected(); float.root.hidden = !picked; if (!picked) return float.body.replaceChildren(); const network = params.network(); const names = lineNames(network.lines); const pieces = network.pieces.filter((piece) => picked.pieces.includes(piece.id)); const lineIds = new Set(pieces.map((piece) => piece.line)); const total = pieces.reduce((sum, piece) => sum + piece.to_m - piece.from_m, 0); const title = float.root.querySelector(".ui-float-panel__title"); if (title) title.textContent = `구간 ${pieces.length} · 선 ${lineIds.size} · ${meters(total)}`; const editable = params.editable(); const scope = createSegment({ options: [ { value: "piece", label: "구간" }, { value: "line", label: "선" }, { value: "year", label: "같은 해" }, ], value: picked.scope, size: 24, ariaLabel: "고름 넓이", onChange: (value) => edit.setScope(value as PlanScope), }); scope.root.dataset.role = "props-scope"; const years = new Set(pieces.map((piece) => piece.year)); const options = yearOptions(); const year = createSelectField({ label: "연도", options: years.size > 1 ? [{ value: "", text: "여러 해" }, ...options] : options, value: years.size > 1 ? "" : String(pieces[0]?.year ?? ""), size: 24, ariaLabel: "고른 구간 연도", disabled: !editable, onChange: (value) => value && params.commit(setPiecesYear(params.network(), picked.pieces, Number(value))), }); year.select.dataset.role = "props-year"; const rows = document.createElement("div"); rows.className = "d03-plan__props-rows"; for (const piece of pieces) { const index = network.lines.findIndex((line) => line.id === piece.line); const row = createButton({ label: `${names[index]} ${station(piece.from_m)}~${station(piece.to_m)} · ` + `${meters(piece.to_m - piece.from_m)} · ${piece.year}년`, variant: "ghost", size: 24, onClick: () => edit.select([piece.id]), }); row.dataset.piece = piece.id; rows.append(row); } const actions = document.createElement("div"); actions.className = "d03-plan__row"; const index = edit.lineIndex(); if (editable && index >= 0) { const flip = createButton({ label: "방향 바꾸기", variant: "ghost", size: 24, onClick: edit.flipLine, }); flip.title = "측점 No.0 쪽(시점 쪽)을 반대 끝으로 바꿉니다."; flip.disabled = !!network.lines[index].branch; flip.dataset.role = "props-flip"; const drop = createButton({ label: "선 지움", variant: "ghost", size: 24, onClick: edit.dropLine, }); drop.dataset.role = "props-drop"; const branch = createButton({ label: "갈래 시점 지정", variant: "ghost", size: 24, onClick: edit.branchLine, }); branch.title = "다른 선에 닿은 끝을 시점(No.0)으로 · 그 선의 갈래로 붙입니다."; branch.hidden = !!network.lines[index].branch; branch.dataset.role = "props-branch"; actions.append(flip, branch, drop); } if (editable && picked.mark) { const dropMark = createButton({ label: "마크 지움", variant: "ghost", size: 24, onClick: edit.dropMark, }); dropMark.dataset.role = "props-drop-mark"; actions.append(dropMark); } float.body.replaceChildren(scope.root, year.root, rows, actions); } /** 좌측 줄 — 누름 = 고름(div · 열쇠 Enter 도). */ const pickRow = (row: HTMLElement, onPick: () => void): HTMLElement => { row.tabIndex = 0; row.setAttribute("role", "button"); row.addEventListener("click", onPick); row.addEventListener("keydown", (event) => event.key === "Enter" && onPick()); return row; }; function renderList(): void { const network = params.network(); const names = lineNames(network.lines); const picked = new Set(edit.selected()?.pieces ?? []); const scope = edit.selected()?.scope; const rows: HTMLElement[] = []; const byYear = new Map(); for (const piece of network.pieces) byYear.set(piece.year, [...(byYear.get(piece.year) ?? []), piece]); for (const [year, pieces] of [...byYear].sort(([a], [b]) => a - b)) { const row = document.createElement("div"); row.className = "d03-plan__piece d03-plan__piece-year"; row.dataset.year = String(year); row.classList.toggle( "is-selected", scope === "year" && pieces.every((p) => picked.has(p.id)), ); const length = pieces.reduce((sum, p) => sum + p.to_m - p.from_m, 0); row.append(createText(`${year}년 · 구간 ${pieces.length} · ${meters(length)}`, "muted")); rows.push( pickRow(row, () => { edit.select([pieces[0].id]); edit.setScope("year"); }), ); } network.lines.forEach((line, index) => { const pieces = network.pieces.filter((piece) => piece.line === line.id); if (!pieces.length) return; const head = document.createElement("div"); head.className = "d03-plan__piece d03-plan__piece-head"; head.dataset.line = line.id; head.classList.toggle( "is-selected", scope === "line" && pieces.every((p) => picked.has(p.id)), ); head.append(createText(`${names[index]} · ${meters(lineLength(line.vertices))}`, "muted")); rows.push( pickRow(head, () => { edit.select([pieces[0].id]); edit.setScope("line"); }), ); for (const piece of pieces) { const row = document.createElement("div"); row.className = "d03-plan__piece"; row.classList.toggle("is-selected", picked.has(piece.id)); row.dataset.piece = piece.id; row.append( createText( `${station(piece.from_m)} ~ ${station(piece.to_m)} (${meters(piece.to_m - piece.from_m)})`, "muted", ), createText(`${piece.year}년`, "muted"), ); rows.push(pickRow(row, () => edit.select([piece.id]))); } }); if (!rows.length) rows.push(createText("선을 그으면 구역이 생깁니다.", "muted")); list.replaceChildren(...rows); } return { panel: float.root, list, render() { renderPanel(); renderList(); }, yearOptions, }; }