/* ============================================================================= * common_util/common_util_route_follow.ts * 노선이 바뀔 때 관 · 구조물이 땅 위 자리(좌표)를 따라감 — 누가거리 다시 매김(344-2). * * 자리 정본 = 좌표(x · y) · 누가거리 = 그 자리를 지금 노선에서 읽은 값(정수 m · 234-1). * 짝: * - `projectPipePoints` ↔ 파이썬 `common_util_drainage_pipes.project_pipe_points`(xy 투영 + 정수 m · * 구간 끝 같은 만큼 밂 · 누가거리 순). * - `followStructures` ↔ 파이썬 `common_util_route_follow.follow_structures`(+ 0 ~ 연장 자름 · * 노선과 거리 · `STRUCTURE_ROUTE_DRIFT_MAX_M` 넘으면 「노선에서 멀어짐」 · 버리지 않음). * - `projectOnPolyline` = shapely `LineString.project`(가장 가까운 첫 선분) · `distance`. * 거울 시험 = resources/tester/test_344_2_route_pairs.py. * ========================================================================== */ import { snapStructureStation } from "./common_util_structure_station"; /** 「노선에서 멀어짐」 거리(m) — 파이썬 `config_system.STRUCTURE_ROUTE_DRIFT_MAX_M` 짝(거울 시험이 대조). */ export const STRUCTURE_ROUTE_DRIFT_MAX_M = 20; export interface XY { x: number; y: number; } /** 노선 위 자리 — 누가거리 · 노선과의 거리. 노선이 없으면(점 둘 미만 · 길이 0) null. */ export function projectOnPolyline( line: ReadonlyArray, point: XY, ): { chainage_m: number; distance_m: number; length_m: number } | null { let walked = 0; let best: { chainage_m: number; distance_m: number } | null = null; for (let index = 1; index < line.length; index += 1) { const a = line[index - 1]; const b = line[index]; const dx = b.x - a.x; const dy = b.y - a.y; const span = Math.hypot(dx, dy); if (span > 0) { const t = Math.min(1, Math.max(0, ((point.x - a.x) * dx + (point.y - a.y) * dy) / span ** 2)); const distance = Math.hypot(a.x + dx * t - point.x, a.y + dy * t - point.y); if (best === null || distance < best.distance_m) best = { chainage_m: walked + span * t, distance_m: distance }; } walked += span; } return best && walked > 0 ? { ...best, length_m: walked } : null; } export interface FollowingPoint { chainage_m: number; x?: number | null; y?: number | null; start_m?: number | null; end_m?: number | null; } /** 저장 좌표를 새 노선에 투영해 누가거리를 다시 매긴 사본(누가거리 순) — 관 지점. */ export function projectPipePoints

( points: ReadonlyArray

, line: ReadonlyArray, ): P[] { const length = line.reduce( (sum, point, index) => index ? sum + Math.hypot(point.x - line[index - 1].x, point.y - line[index - 1].y) : 0, 0, ); if (!(length > 0)) return [...points]; // 파이썬도 노선이 없으면 그대로(차례도) return points .map((point) => { if (point.x == null || point.y == null) return { ...point }; const hit = projectOnPolyline(line, { x: point.x, y: point.y })!; const moved = snapStructureStation(hit.chainage_m, [hit.length_m]); const shift = moved - point.chainage_m; return { ...point, chainage_m: moved, ...(point.start_m != null ? { start_m: point.start_m + shift } : {}), ...(point.end_m != null ? { end_m: point.end_m + shift } : {}), }; }) .sort((a, b) => a.chainage_m - b.chainage_m); } export interface FollowingStructure { chainage_m?: number | null; start_m?: number | null; end_m?: number | null; x?: number | null; y?: number | null; } /** 구조물이 좌표를 따라감 — 사본 · 노선과 거리(좌표 없으면 null) · 멀어짐(차례는 그대로). */ export function followStructures( items: ReadonlyArray, line: ReadonlyArray, maxDriftM: number = STRUCTURE_ROUTE_DRIFT_MAX_M, ): Array<{ item: S; drift_m: number | null; far: boolean }> { return items.map((item) => { const hit = item.x != null && item.y != null ? projectOnPolyline(line, { x: item.x, y: item.y }) : null; if (hit === null || item.chainage_m == null) return { item: { ...item }, drift_m: null, far: false }; const shift = snapStructureStation(hit.chainage_m, [hit.length_m]) - item.chainage_m; const clamp = (value: number): number => Math.min(Math.max(value + shift, 0), hit.length_m); return { item: { ...item, chainage_m: clamp(item.chainage_m), ...(item.start_m != null ? { start_m: clamp(item.start_m) } : {}), ...(item.end_m != null ? { end_m: clamp(item.end_m) } : {}), }, drift_m: hit.distance_m, far: hit.distance_m > maxDriftM, }; }); }