/* ============================================================================= * B05_Profile_PlanContext.ts * B05 흐린 다른 세트 셈(PLAN 635-4 · 요구 546 · 547 · 결정 5 · 6 · 7) — 순수 함수. * 입력 = `GET /api/projects/{B}/plan-context`(같은 예정노선 세트 모두 · B 좌표 · 업체 이름 없음). * 흐릴 세트 = 내 세트 아님 · 이음점 = 남 세트 꺾은선 끝 · 받을 때 이어진 이음점 = 내 받은 선 끝 1 m 안. * 이음은 고정 안 함 — 벗어나면 안내 글 한 줄(판단 = 설계자). * ========================================================================== */ import { lineLength, pointAtDistance } from "../../D03_PlanAnalysis/logic/D03_PlanAnalysis_Draw"; export type Vertex = [number, number]; /** plan-context 세트 한 줄(635-2 꼴). */ export interface ContextSet { key: string; year: number; no: number; mine: boolean; parts: number; branch: boolean; length_m: number; lines: Vertex[][]; } export interface PlanContext { linked: boolean; plan_name?: string; mine?: string | null; sets?: ContextSet[]; } /** 이음점 · 끝 맞음 거리(m). */ export const JOINT_TOL_M = 1; export interface Joint { key: string; point: Vertex; } const dist = (a: Vertex, b: Vertex): number => Math.hypot(a[0] - b[0], a[1] - b[1]); const drawn = (line: Vertex[]): boolean => line.length > 1; const ends = (line: Vertex[]): Vertex[] => [line[0], line[line.length - 1]]; /** 흐릴 세트 — 내 세트 아님 · 그릴 선 있음. */ export function otherSets(context: PlanContext | null): ContextSet[] { if (!context?.linked) return []; return (context.sets ?? []).filter((set) => !set.mine && set.lines.some(drawn)); } /** 남 세트 꺾은선 끝 모두(붙을 자리). */ export function jointPoints(context: PlanContext | null): Joint[] { return otherSets(context).flatMap((set) => set.lines.filter(drawn).flatMap((line) => ends(line).map((point) => ({ key: set.key, point }))), ); } /** 받을 때 이어진 이음점 — 남 세트 끝 중 내 받은 선 끝 1 m 안. */ export function linkedJoints(context: PlanContext | null): Joint[] { const mine = (context?.sets ?? []).find((set) => set.mine); const tips = (mine?.lines ?? []).filter(drawn).flatMap(ends); return jointPoints(context).filter((joint) => tips.some((tip) => dist(tip, joint.point) <= JOINT_TOL_M), ); } /** 이어졌던 이음점마다 지금 선 끝까지 거리 — 1 m 밖만 · 선 없음 = 빈. */ export function jointGaps( line: ReadonlyArray, joints: ReadonlyArray, ): Array<{ key: string; gapM: number }> { if (line.length < 2) return []; const tips = ends(line as Vertex[]); return joints .map((joint) => ({ key: joint.key, gapM: Math.min(...tips.map((tip) => dist(tip, joint.point))), })) .filter((gap) => gap.gapM > JOINT_TOL_M); } /** 벗어남 안내 한 줄(없음 = null). */ export function jointNote(gaps: ReadonlyArray<{ key: string; gapM: number }>): string | null { if (!gaps.length) return null; const list = gaps.map((gap) => `${gap.key} ${gap.gapM.toFixed(1)} m`).join(" · "); return `이웃 세트 이음점에서 벗어남(${list}) — 이음은 고정이 아님 · 끝을 이음점에 놓으면 붙습니다.`; } /** 세트 이름표 자리 — 가장 긴 꺾은선 가운데. */ export function labelPoint(set: ContextSet): Vertex | null { const line = set.lines.filter(drawn).sort((a, b) => lineLength(b) - lineLength(a))[0]; return line ? pointAtDistance(line, lineLength(line) / 2) : null; }