- UI_Points: 우클릭 메뉴 = UI_Menu 그대로 · sub4 597-2 칸 몫([추가] · 펼침 이름 · 경위도 · nameError · 칸 설명 글 걷음) 살림 · 핀 이름 그림(pointLayer names) · dev 쪽 openMenu · mount 걷음(지점 놓기 노드 붙음 = UI_Menu place) - UI_Pieces: 구역 지움 = edit.dropRegion(line.id)(598 이 dropLine 을 dropRegion 으로 바꿈) - UI_Page: 지점 한 걸음 stepPoints — 위치(site) 같이 실음(596-2 Snap) · 700 줄 - UI_Edit: 고름 강조 선 = strokeCased 검은 테(600-1 · 밝은 바탕 대비) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
8.9 KiB
TypeScript
234 lines
8.9 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_Pieces.ts
|
|
* D03 좌측 「구역 · 연도」 목록 몸(PLAN 597 · 요구 463 14) — 구역 = 그은 꺾은선 하나(본선 · 갈래).
|
|
* 해 줄(같은 해) · 구역 줄(색 띠 = 지도 선과 같은 `routeColor(차례)` · 이름 칸 · 연도 고름 · 지움) ·
|
|
* 구간 줄(측점 글 작게 · 연도 고름). 누름 = 고름 · 해 고침 · 이름 · 지움은 한 걸음(되돌리기 · 캐시 = 페이지).
|
|
* 연도 고름은 속성 판과 같은 셈(`setLineYear` · `setPiecesYear`) · 보기만 계정 = 값만.
|
|
* ========================================================================== */
|
|
|
|
import { createIconButton } from "@ui/ui_template_button";
|
|
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
|
import { createListRow } from "@ui/ui_template_list_row";
|
|
import { askConfirm } from "@ui/ui_template_modal";
|
|
import { routeColor } from "@ui/ui_template_palette";
|
|
import { stationLabel } from "../common_util/common_util_station";
|
|
import { lineLength } from "./logic/D03_PlanAnalysis_Draw";
|
|
import {
|
|
lineNames,
|
|
nameError,
|
|
setLineYear,
|
|
setPiecesYear,
|
|
type PlanNetwork,
|
|
} from "./logic/D03_PlanAnalysis_Plan";
|
|
import { setLineName } from "./logic/D03_PlanAnalysis_PointEdit";
|
|
import type { PlanEdit } from "./D03_PlanAnalysis_UI_Edit";
|
|
import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map";
|
|
|
|
const station = (m: number): string => stationLabel(m, PLAN_STATION_INTERVAL_M, 1);
|
|
const meters = (m: number): string => `${m.toFixed(1)} m`;
|
|
|
|
export interface PlanPiecesParams {
|
|
network: () => PlanNetwork;
|
|
edit: PlanEdit;
|
|
editable: () => boolean;
|
|
/** 고를 해 — 사업연도 시작부터 열 해 + 조각에 있는 해. */
|
|
yearOptions: () => Array<{ value: string; text: string }>;
|
|
commit: (next: PlanNetwork) => void;
|
|
}
|
|
|
|
export interface PlanPieces {
|
|
/** 좌측 「구역 · 연도」 목록 몸. */
|
|
list: HTMLElement;
|
|
render: () => void;
|
|
}
|
|
|
|
export function createPlanPieces(params: PlanPiecesParams): PlanPieces {
|
|
const { edit } = params;
|
|
const list = document.createElement("div");
|
|
list.className = "d03-plan__pieces";
|
|
let shown = ""; // 지금 그린 틀 열쇠 — 같으면 값만 갈아 끼움(칸 초점 지키기)
|
|
let syncs: Array<() => void> = [];
|
|
|
|
/** 줄 안 칸 눌림은 줄 고름으로 안 번지게. */
|
|
const isControl = (event: Event): boolean =>
|
|
!!(event.target as Element).closest("input, select, button, .ui-list-row__trailing");
|
|
|
|
/** 줄 하나 — 누름 = 고름(열쇠 Enter 도) · 줄 안 칸은 제외. */
|
|
const pickable = (row: HTMLElement, onPick: () => void): HTMLElement => {
|
|
row.tabIndex = 0;
|
|
row.addEventListener("click", (event) => isControl(event) || onPick());
|
|
row.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" && event.target === row) onPick();
|
|
});
|
|
return row;
|
|
};
|
|
|
|
/** 연도 고름 칸 — 값이 섞이면 「여러 해」. */
|
|
function yearSelect(
|
|
years: () => number[],
|
|
label: string,
|
|
pick: (year: number) => void,
|
|
): { root: HTMLElement; sync: () => void } {
|
|
const field = createSelectField({
|
|
options: [],
|
|
size: 24,
|
|
ariaLabel: label,
|
|
disabled: !params.editable(),
|
|
onChange: (value) => value && pick(Number(value)),
|
|
});
|
|
const sync = (): void => {
|
|
const set = new Set(years());
|
|
const options = params.yearOptions();
|
|
field.setOptions(
|
|
set.size > 1 ? [{ value: "", text: "여러 해" }, ...options] : options,
|
|
set.size > 1 ? "" : String([...set][0]),
|
|
);
|
|
};
|
|
sync();
|
|
return { root: field.root, sync };
|
|
}
|
|
|
|
function render(): void {
|
|
const network = params.network();
|
|
const names = lineNames(network.lines);
|
|
const picked = new Set(edit.selected()?.pieces ?? []);
|
|
const scope = edit.selected()?.scope;
|
|
const editable = params.editable();
|
|
const key = JSON.stringify([
|
|
network.lines.map((line) => line.id),
|
|
network.pieces.map((piece) => [piece.id, piece.line]),
|
|
[...picked],
|
|
scope,
|
|
editable,
|
|
params.yearOptions().length,
|
|
]);
|
|
if (key === shown) {
|
|
syncs.forEach((sync) => sync());
|
|
return;
|
|
}
|
|
shown = key;
|
|
syncs = [];
|
|
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 length = pieces.reduce((sum, p) => sum + p.to_m - p.from_m, 0);
|
|
const row = createListRow({
|
|
label: `${year}년`,
|
|
sub: `구간 ${pieces.length} · ${meters(length)}`,
|
|
size: 28,
|
|
active: scope === "year" && pieces.every((p) => picked.has(p.id)),
|
|
onClick: () => {
|
|
edit.select([pieces[0].id]);
|
|
edit.setScope("year");
|
|
},
|
|
});
|
|
row.dataset.year = String(year);
|
|
rows.push(row);
|
|
}
|
|
|
|
network.lines.forEach((line, index) => {
|
|
const pieces = network.pieces.filter((piece) => piece.line === line.id);
|
|
if (!pieces.length) return;
|
|
const lineNow = (): (typeof network.lines)[number] =>
|
|
params.network().lines.find((entry) => entry.id === line.id) ?? line;
|
|
const piecesNow = (): typeof pieces =>
|
|
params.network().pieces.filter((piece) => piece.line === line.id);
|
|
|
|
const nameField = createInputField({ size: 24, placeholder: names[index] });
|
|
nameField.input.value = line.name ?? "";
|
|
nameField.input.setAttribute("aria-label", "구역 이름");
|
|
nameField.input.dataset.role = "line-name";
|
|
nameField.input.disabled = !editable;
|
|
nameField.input.addEventListener("input", () =>
|
|
nameField.setError(nameError(nameField.input.value) ?? undefined),
|
|
);
|
|
nameField.input.addEventListener("change", () => {
|
|
const error = nameError(nameField.input.value);
|
|
nameField.setError(error ?? undefined);
|
|
const text = nameField.input.value.trim();
|
|
if (!error && text !== (lineNow().name ?? ""))
|
|
params.commit(setLineName(params.network(), line.id, text));
|
|
});
|
|
|
|
const year = yearSelect(
|
|
() => piecesNow().map((piece) => piece.year),
|
|
"구역 연도",
|
|
(value) => params.commit(setLineYear(params.network(), line.id, value)),
|
|
);
|
|
year.root.dataset.role = "line-year";
|
|
const drop = createIconButton({
|
|
icon: "trash",
|
|
title: "구역 지움",
|
|
size: 24,
|
|
onClick: async () => {
|
|
const ok = await askConfirm({
|
|
message: `「${lineNames(params.network().lines)[index]}」 구역을 지울까요?`,
|
|
okLabel: "지움",
|
|
danger: true,
|
|
});
|
|
if (!ok) return;
|
|
edit.dropRegion(line.id);
|
|
},
|
|
});
|
|
drop.dataset.role = "line-drop";
|
|
drop.hidden = !editable;
|
|
const tail = document.createElement("span");
|
|
tail.className = "d03-plan__line-tail";
|
|
tail.append(year.root, drop);
|
|
|
|
const head = createListRow({
|
|
label: "",
|
|
sub: `구간 ${pieces.length} · ${meters(lineLength(line.vertices))}`,
|
|
stripe: routeColor(index),
|
|
size: 28,
|
|
trailing: tail,
|
|
active: scope === "line" && pieces.every((p) => picked.has(p.id)),
|
|
});
|
|
head.querySelector(".ui-list-row__label")?.replaceChildren(nameField.root);
|
|
head.dataset.line = line.id;
|
|
rows.push(
|
|
pickable(head, () => {
|
|
edit.select([pieces[0].id]);
|
|
edit.setScope("line");
|
|
}),
|
|
);
|
|
syncs.push(() => {
|
|
year.sync();
|
|
nameField.input.placeholder = lineNames(params.network().lines)[index] ?? "";
|
|
if (document.activeElement !== nameField.input)
|
|
nameField.input.value = lineNow().name ?? "";
|
|
const sub = head.querySelector(".ui-list-row__sub");
|
|
if (sub)
|
|
sub.textContent = `구간 ${piecesNow().length} · ${meters(lineLength(lineNow().vertices))}`;
|
|
});
|
|
|
|
for (const piece of pieces) {
|
|
const pieceNow = (): (typeof pieces)[number] =>
|
|
params.network().pieces.find((entry) => entry.id === piece.id) ?? piece;
|
|
const pieceYear = yearSelect(
|
|
() => [pieceNow().year],
|
|
"구간 연도",
|
|
(value) => params.commit(setPiecesYear(params.network(), [piece.id], value)),
|
|
);
|
|
const row = createListRow({
|
|
label: `${station(piece.from_m)} ~ ${station(piece.to_m)}`,
|
|
sub: meters(piece.to_m - piece.from_m),
|
|
size: 28,
|
|
trailing: pieceYear.root,
|
|
active: picked.has(piece.id),
|
|
});
|
|
row.classList.add("d03-plan__piece-row");
|
|
row.dataset.piece = piece.id;
|
|
rows.push(pickable(row, () => edit.select([piece.id])));
|
|
syncs.push(pieceYear.sync);
|
|
}
|
|
});
|
|
list.replaceChildren(...rows);
|
|
}
|
|
|
|
return { list, render };
|
|
}
|