Files
Aislo/D03_PlanAnalysis/logic/D03_PlanAnalysis_Measure.ts
T
eomsangdonandClaude Sonnet 5.5 7ae441be67 feat(D 예정노선): 거리재기 첫 점이 노선 위면 노선 따라 길이 (PLAN 600-2 · 요구 463-23)
- 새 순수 셈 logic/D03_PlanAnalysis_Measure — 같은 선 누가 차 · 갈래 고리 따라 올라가 만남 · 이어지지 않으면 곧은 거리
- 재기 겹 — 첫 누름 선 8 px · 꺾임점 12 px 안이면 노선 따라 · 굵게 그림 · 글 「노선 따라 L m · 측점 · Δh」
- Page.ts 한 줄(노선 넘김) · 새 시험 test_600_2_plan_measure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:40:29 +09:00

98 lines
3.9 KiB
TypeScript

/* =============================================================================
* 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;
}