- 고름 = 구간 묶음 + 넓이 — 선 누름 = 구간 · 다시 누름 = 선 → 같은 해 → 구간 · Shift+누름 = 더함 · 뺌 - 빈 곳 끌기 = 오른쪽으로 창(실선 · 다 든 구간) · 왼쪽으로 걸침(점선 · 걸친 구간까지) · 돌린 지도도 보이는 화면 기준 - 지도 위 속성 판(새 _UI_Props · 공용 떠 있는 판) — 넓이 · 연도(여러 구간 한 번에) · 구간 줄 · 방향 바꾸기 · 선 지움 · 마크 지움 - 좌측 구역 · 연도 = 해 줄 · 선 줄 · 구간 줄 누름 = 고르기(해 고침은 속성 판 한 곳) · 고른 선 위 끌기 = 꺾임점 더해 옮김 - Delete = 고른 마크 · 선 통째 고름이면 그 선 지움 - 시험: 585-2 새 지킴 시험(창 · 걸침 · 돌린 화면 · 여러 구간 연도) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
9.3 KiB
TypeScript
240 lines
9.3 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_Props.ts
|
|
* D03 고른 구간 속성 판 · 좌측 「구역 · 연도」 목록(PLAN 585-2 · 요구서 445 A4) — 오토캐드 특성 창 꼴.
|
|
*
|
|
* 속성 판 = 지도 위 떠 있는 판(공용 `createFloatPanel` · 머리 끌어 옮김 · 오른쪽 위) · 고름 있을 때만 —
|
|
* 머리 「구간 N · 선 M · L m」 · ✕ = 풂 · 넓이 [구간][선][같은 해] · 연도(모두 같으면 그 해 · 다르면 「여러 해」 →
|
|
* 고르면 고른 구간 모두 그 해 = 되돌리기 한 걸음) · 고른 구간 줄(누름 = 그 구간만) ·
|
|
* [방향 바꾸기] [선 지움](선 하나일 때) · [마크 지움](마크 고름) · 보기만 계정 = 값만.
|
|
* 좌측 목록 = 보기 + 고르기만(해 고침은 속성 판 한 곳) — 해 줄(같은 해) · 선 머리 줄(선) · 구간 줄(구간).
|
|
* ========================================================================== */
|
|
|
|
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";
|
|
actions.append(flip, 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<number, typeof network.pieces>();
|
|
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,
|
|
};
|
|
}
|