Files
Aislo/B04_PreProcess/B04_PreProcess_UI_RouteSamples.ts
eomsangdonandClaude Fable 5 f7528a4aa4 refactor(B04): B04_wf1_Surface -> B04_PreProcess 전면 개명
- 폴더·내부 파일 51개 접두사 개명 (git mv, 이력 보존)
- 저장소 전체 참조 치환 67파일: import 경로, 라우트 슬러그(b04-preprocess),
  라우트 키(B04_PREPROCESS), storage 경로 상수, locale, SQL 주석
- 로직 변경 없음 (기계적 치환). typecheck·백엔드 import 검증 통과

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 10:01:36 +09:00

70 lines
2.8 KiB
TypeScript

/* =============================================================================
* 계획선 1m 재표본 (B04 2D 지도 공용)
*
* 백엔드는 도로 위 값을 전부 **누가거리 1m 구간**으로 준다(흐름 강도 곡선, 관 매설 지점,
* 유입 집중점). 화면에서 그 값을 계획선 위에 얹으려면 같은 규칙으로 다시 찍은 점 목록이
* 있어야 한다 — 오버레이마다 따로 찍으면 한 칸씩 밀려 색과 마커가 어긋난다.
* ========================================================================== */
export type RoutePoint = { x: number; y: number };
/** 계획선을 1m 간격으로 다시 찍는다. 배열 인덱스 = 누가거리(m). */
export function resampleRoute(points: ReadonlyArray<RoutePoint>): RoutePoint[] {
const samples: RoutePoint[] = [];
if (points.length < 2) return samples;
let carried = 0;
samples.push({ x: points[0].x, y: points[0].y });
for (let i = 1; i < points.length; i += 1) {
const from = points[i - 1];
const to = points[i];
const dx = to.x - from.x;
const dy = to.y - from.y;
const length = Math.hypot(dx, dy);
if (length <= 0) continue;
let travelled = 1 - carried;
while (travelled <= length) {
samples.push({
x: from.x + (dx * travelled) / length,
y: from.y + (dy * travelled) / length,
});
travelled += 1;
}
carried = (carried + length) % 1;
}
return samples;
}
/** 누가거리(m) 위치의 계획선 좌표. 범위를 벗어나면 양 끝으로 자른다. */
export function pointAtChainage(
samples: ReadonlyArray<RoutePoint>,
chainage: number,
): RoutePoint | null {
if (samples.length === 0) return null;
const index = Math.min(samples.length - 1, Math.max(0, Math.round(chainage)));
return samples[index];
}
/** 화면 좌표에서 가장 가까운 계획선 위치를 찾는다. (누가거리 m, 화면 거리 px).
* 관을 우클릭으로 추가하거나 끌어 옮길 때 "계획선 위"로 스냅하는 근거다. 1m 표본을 전부
* 훑되 화면 변환은 넘겨받은 함수에 맡긴다 — 배경지도 메타를 여기서 알 필요가 없다. */
export function nearestChainage(
samples: ReadonlyArray<RoutePoint>,
toScreen: (point: RoutePoint) => [number, number],
screenX: number,
screenY: number,
): { chainage: number; distance: number } | null {
if (samples.length === 0) return null;
let bestIndex = -1;
let bestDistance = Number.POSITIVE_INFINITY;
for (let index = 0; index < samples.length; index += 1) {
const [x, y] = toScreen(samples[index]);
const distance = Math.hypot(x - screenX, y - screenY);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = index;
}
}
return bestIndex < 0 ? null : { chainage: bestIndex, distance: bestDistance };
}