- 새 순수 셈 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>
98 lines
3.9 KiB
TypeScript
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;
|
|
}
|