/* ============================================================================= * D03_PlanAnalysis_Measure.ts * D 예정노선 거리재기 「노선 따라」(PLAN 600-2 · 요구 463-23) — 첫 누름이 노선 위면 둘째 누름까지 * 노선(망)을 따라간 길 · 같은 선 = 누가 차 · 갈래 ↔ 본선 = 갈래 고리(`branch`)를 따라 올라가 만나는 점까지. * DOM · fetch · sessionStorage 없음 · 길이 셈 = 562-1 짝(`D03_PlanAnalysis_Draw`). * 서로 이어지지 않은 본선끼리 = 길 없음(null) → 부르는 쪽이 곧은 거리로. * ========================================================================== */ import { distanceAlong, lineLength, pointAtDistance, segmentLength, sliceLine, } from "./D03_PlanAnalysis_Draw"; import type { PlanNetwork, Vertex } from "./D03_PlanAnalysis_Plan"; /** 노선 위 한 자리 — 선 id · 그 선 위 누가(m). */ export interface RoutePos { line: string; atM: number; } export interface RouteSpan { lengthM: number; /** 두 자리 사이 노선 따라 꺾은선(앞 자리 → 뒤 자리). */ path: Vertex[]; /** 같은 선 위일 때만 — 두 누가(m). */ stations: [number, number] | null; } /** 자리에서 시작해 갈래 고리를 따라 올라간 (선 · 그 선에 들어선 누가) 목록 — 첫 항목 = 자리 자신. */ function upChain(network: PlanNetwork, pos: RoutePos): RoutePos[] { const chain: RoutePos[] = [pos]; const seen = new Set([pos.line]); let line = network.lines.find((entry) => entry.id === pos.line); while (line?.branch) { const parent = network.lines.find((entry) => entry.id === line!.branch!.line); if (!parent || seen.has(parent.id)) break; seen.add(parent.id); chain.push({ line: parent.id, atM: lineLength(parent.vertices.slice(0, line.branch.vertex + 1)), }); line = parent; } return chain; } /** 두 자리 사이 노선 따라 길 — 이어지지 않으면 null. */ export function routeSpan(network: PlanNetwork, a: RoutePos, b: RoutePos): RouteSpan | null { const up = upChain(network, a); const down = upChain(network, b); const ia = up.findIndex((step) => down.some((other) => other.line === step.line)); if (ia < 0) return null; const ib = down.findIndex((step) => step.line === up[ia].line); const legs: Array<[string, number, number]> = []; // a → 갈래 첫 점(0 m) → 붙은 선 … 만나는 선 for (let k = 0; k < ia; k += 1) legs.push([up[k].line, up[k].atM, 0]); legs.push([up[ia].line, up[ia].atM, down[ib].atM]); // 만나는 선 → … → b for (let k = ib - 1; k >= 0; k -= 1) legs.push([down[k].line, 0, down[k].atM]); const path: Vertex[] = []; let lengthM = 0; for (const [id, from, to] of legs) { const vertices = network.lines.find((entry) => entry.id === id)?.vertices ?? []; if (vertices.length < 2) continue; lengthM += Math.abs(to - from); const piece = sliceLine(vertices, from, to); if (from > to) piece.reverse(); for (const point of piece) { const last = path[path.length - 1]; if (!last || segmentLength(last, point) > 1e-9) path.push(point); } } const same = ia === 0 && ib === 0; return { lengthM, path, stations: same ? [a.atM, b.atM] : null }; } /** 점에서 가장 가까운 노선 위 자리 — `from` 과 이어진 선 안에서만. */ export function nearestOnRoute( network: PlanNetwork, point: Vertex, from: RoutePos, ): (RoutePos & { point: Vertex }) | null { let best: (RoutePos & { point: Vertex }) | null = null; let bestGap = Infinity; for (const line of network.lines) { if (line.vertices.length < 2) continue; const atM = distanceAlong(line.vertices, point); const gap = segmentLength(pointAtDistance(line.vertices, atM), point); if (gap >= bestGap || !routeSpan(network, from, { line: line.id, atM })) continue; best = { line: line.id, atM, point: pointAtDistance(line.vertices, atM) }; bestGap = gap; } return best; }