- 새 logic/D03_PlanAnalysis_Group.ts: assignSet(고른 구간 끝 나눔 → 세트 → 같은 선 이웃 같은 세트 하나 · 앞 id) · mergeSets(서버 Network.derive 짝) · setChoices(있는 세트 + 해마다 새 번호) · joinEnds(닿음 바로 · 떨어짐 곧은 토막 덧 = 앞 선 세트) - Plan.ts 세 곳: PlanPiece.set · 새 선 처음 세트(사업연도 시작 해 새 번호 · 갈래 = 붙은 자리 세트 · 옛 꼴 = 그 해 최댓값 + 1) · 이음 합침 비교 = 세트 · 661 줄 그대로 - Select.ts 끌어 고르기 시험 조각에 set 칸(타입 맞춤) - Api.ts 꼴: derived.sets(PlanSetView · 638-1 서버 셈) - 시험 test_638_group.py · 거울 사례 fixtures/d03_638_set_cases.json(sub3 638-1 과 같은 파일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
7.5 KiB
TypeScript
170 lines
7.5 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_Group.ts
|
|
* D 예정노선 노선 세트(PLAN 638-2 · 요구 547 · 548) — 세트 = 「연도-번호」(2027-1) · 같은 번호 = 갈라져 있어도 같은 노선.
|
|
* 세트 지정(고른 구간 끝에서 조각 나눔 → 세트 바꿈 → 같은 선 이웃 같은 세트 하나로) · 고를 세트 목록 ·
|
|
* [잇기](두 선 끝 · 닿으면 바로 · 떨어지면 곧은 토막 덧 · 이은 토막 = 앞 선 세트).
|
|
* 새 선 처음 세트 · 갈래 · 이어 긋기 = `D03_PlanAnalysis_Plan.normalizePieces`.
|
|
* ⚠ 짝 = `D03_PlanAnalysis_Network.derive` 세트 합침(서버 [저장] 정본) — 거울 시험 638(`fixtures/d03_638_set_cases.json`).
|
|
* 끊김 · 갈래 경고 셈은 서버 한 곳(여기 없음). DOM · fetch · sessionStorage 없음.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
appendVertex,
|
|
attachHead,
|
|
nextSet,
|
|
normalizePieces,
|
|
reverseLine,
|
|
samePoint,
|
|
splitPiece,
|
|
type PlanNetwork,
|
|
type PlanPiece,
|
|
} from "./D03_PlanAnalysis_Plan";
|
|
|
|
/** 세트 — 연도 · 그 해 번호(1 부터). */
|
|
export interface PieceSet {
|
|
year: number;
|
|
set: number;
|
|
}
|
|
|
|
/** 이웃 조각이 맞닿았다고 볼 틈(m) — 서버 합침과 같음(1 cm). */
|
|
const TOUCH_M = 0.01;
|
|
|
|
/** 세트 열쇠 「연도-번호」(업체 지정 · B 받기 단위). */
|
|
export const setKey = (s: PieceSet): string => `${s.year}-${s.set}`;
|
|
|
|
export const sameSet = (a: PieceSet, b: PieceSet): boolean => a.year === b.year && a.set === b.set;
|
|
|
|
/** 같은 선 이웃(from_m 차례 · 맞닿음) 같은 세트 = 하나(앞 조각 id · 사이 마크 걷음) · 다른 선끼리 안 합침 ·
|
|
* `lines` = 이 선만(없으면 모두) · 결과 = 선 차례 · from_m 차례. */
|
|
export function mergeSets(network: PlanNetwork, lines?: ReadonlySet<string>): PlanNetwork {
|
|
const pieces: PlanPiece[] = [];
|
|
for (const line of network.lines) {
|
|
const own = network.pieces
|
|
.filter((piece) => piece.line === line.id)
|
|
.sort((a, b) => a.from_m - b.from_m);
|
|
for (const piece of own) {
|
|
const last = pieces[pieces.length - 1];
|
|
const join =
|
|
(!lines || lines.has(line.id)) &&
|
|
last?.line === line.id &&
|
|
sameSet(last, piece) &&
|
|
Math.abs(last.to_m - piece.from_m) <= TOUCH_M;
|
|
if (join) pieces[pieces.length - 1] = { ...last, to_m: piece.to_m };
|
|
else pieces.push({ ...piece });
|
|
}
|
|
}
|
|
return { ...network, pieces };
|
|
}
|
|
|
|
/** 선 위 구간(m). */
|
|
export interface LineRange {
|
|
line: string;
|
|
from_m: number;
|
|
to_m: number;
|
|
}
|
|
|
|
/** 세트 지정 — 고른 구간 끝에서 조각을 나눔(끝 · 경계와 1 m 안이면 그 경계) → 가운데가 구간 안인 조각 = `target` →
|
|
* 고친 선 이웃 같은 세트 하나로. 연도를 바꾸면 `target` = 그 해 새 세트(`setChoices`). */
|
|
export function assignSet(
|
|
network: PlanNetwork,
|
|
ranges: ReadonlyArray<LineRange>,
|
|
target: PieceSet,
|
|
): PlanNetwork {
|
|
let next = network;
|
|
for (const range of ranges)
|
|
for (const at of [range.from_m, range.to_m]) next = splitPiece(next, range.line, at);
|
|
const inside = (piece: PlanPiece): boolean =>
|
|
ranges.some((range) => {
|
|
const mid = (piece.from_m + piece.to_m) / 2;
|
|
const [a, b] = [Math.min(range.from_m, range.to_m), Math.max(range.from_m, range.to_m)];
|
|
return range.line === piece.line && mid > a && mid < b;
|
|
});
|
|
const pieces = next.pieces.map((piece) =>
|
|
inside(piece) ? { ...piece, year: target.year, set: target.set } : piece,
|
|
);
|
|
return mergeSets({ ...next, pieces }, new Set(ranges.map((range) => range.line)));
|
|
}
|
|
|
|
/** 고를 세트 — 있는 세트 + 해마다 「새 세트」(`fresh` · 번호 = 그 해 최댓값 + 1) · 연도 · 번호 차례. */
|
|
export interface SetChoice extends PieceSet {
|
|
key: string;
|
|
fresh: boolean;
|
|
}
|
|
|
|
export function setChoices(network: PlanNetwork, years: ReadonlyArray<number>): SetChoice[] {
|
|
const have = new Map<string, PieceSet>();
|
|
for (const piece of network.pieces)
|
|
if (piece.set > 0) have.set(setKey(piece), { year: piece.year, set: piece.set });
|
|
const allYears = new Set([...years, ...[...have.values()].map((s) => s.year)]);
|
|
const out: SetChoice[] = [];
|
|
for (const year of [...allYears].sort((a, b) => a - b)) {
|
|
const own = [...have.values()].filter((s) => s.year === year).sort((a, b) => a.set - b.set);
|
|
for (const s of own) out.push({ ...s, key: setKey(s), fresh: false });
|
|
const fresh = { year, set: nextSet(network.pieces, year) };
|
|
out.push({ ...fresh, key: setKey(fresh), fresh: true });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 선 끝 — 선 id · 머리(첫 점) 쪽인가. */
|
|
export interface LineEnd {
|
|
line: string;
|
|
head: boolean;
|
|
}
|
|
|
|
const indexOf = (network: PlanNetwork, id: string): number =>
|
|
network.lines.findIndex((line) => line.id === id);
|
|
|
|
const endPoint = (network: PlanNetwork, end: LineEnd): [number, number] => {
|
|
const v = network.lines[indexOf(network, end.line)].vertices;
|
|
return end.head ? v[0] : v[v.length - 1];
|
|
};
|
|
|
|
/** `id` 선 방향 바꿈(갈래 = null). */
|
|
function flip(network: PlanNetwork, id: string): PlanNetwork | null {
|
|
const out = reverseLine(network, indexOf(network, id));
|
|
return typeof out === "string" ? null : out;
|
|
}
|
|
|
|
/** `x` 끝이 꼬리 · `y` 끝이 머리가 되게 돌려 `y` 를 `x` 꼬리에 이음 — 못 하면 null. */
|
|
function joinAt(network: PlanNetwork, x: LineEnd, y: LineEnd): PlanNetwork | null {
|
|
let next: PlanNetwork | null = network;
|
|
if (x.head) next = flip(next, x.line);
|
|
if (next && !y.head) next = flip(next, y.line);
|
|
if (!next) return null;
|
|
const xn = next.lines[indexOf(next, x.line)].vertices.length;
|
|
const yn = next.lines[indexOf(next, y.line)].vertices.length;
|
|
const joined = attachHead(next, indexOf(next, y.line));
|
|
const after = joined.lines[indexOf(joined, x.line)];
|
|
// 다른 선 꼬리에 붙거나 갈래가 됨 = 이 두 선 이음이 아님
|
|
return indexOf(joined, y.line) < 0 && after?.vertices.length === xn + yn - 1 ? joined : null;
|
|
}
|
|
|
|
/** [잇기] — 두 선 끝을 한 선으로 · 떨어져 있으면 `a` 끝에서 `b` 끝까지 곧은 토막을 덧댐(그 토막 = `a` 끝 조각 세트) ·
|
|
* 이음 자리 두 조각이 같은 세트면 하나 · 못 이음 = 까닭 글. `year` = 새 선 처음 해(`normalizePieces`). */
|
|
export function joinEnds(
|
|
network: PlanNetwork,
|
|
a: LineEnd,
|
|
b: LineEnd,
|
|
year: number,
|
|
): PlanNetwork | string {
|
|
const [ia, ib] = [indexOf(network, a.line), indexOf(network, b.line)];
|
|
if (ia < 0 || ib < 0) return "이을 선이 없습니다.";
|
|
if (ia === ib) return "같은 선의 두 끝은 이을 수 없습니다.";
|
|
const isBranch = (end: LineEnd): boolean =>
|
|
end.head && !!network.lines[indexOf(network, end.line)].branch;
|
|
if (isBranch(a) || isBranch(b)) return "갈래 첫 점은 붙은 선 위라 이을 수 없습니다.";
|
|
let next = network;
|
|
const to = endPoint(network, b);
|
|
if (!samePoint(endPoint(network, a), to)) {
|
|
// `a` 끝에 곧은 토막 — 머리 쪽이면 뒤집어 꼬리에 덧대고 다시 뒤집음(조각이 따라감)
|
|
if (a.head) next = flip(next, a.line) ?? next;
|
|
next = normalizePieces(appendVertex(next, indexOf(next, a.line), [to[0], to[1]]), year);
|
|
if (a.head) next = flip(next, a.line) ?? next;
|
|
}
|
|
// 덜 뒤집는 쪽 먼저(머리 · 꼬리면 뒤 선 꼬리에 앞 선)
|
|
const [x, y] = a.head && !b.head ? [b, a] : [a, b];
|
|
const joined = joinAt(next, x, y) ?? joinAt(next, y, x);
|
|
return joined ? normalizePieces(joined, year) : "두 선을 이을 수 없습니다 — 선 끝을 고르세요.";
|
|
}
|