feat(344-2): 첫 계획선 · 관 자리 투영 · 구조물 좌표 따라감 · 계획고 옮김 짝
- 첫 계획선(계획고 = 지반고) base_pvi TS 짝 = B05_Profile/logic/B05_Profile_Grade_Ground.ts(1순위만 · 못 서는 땅은 null = 서버로 다시 셈) - 관 자리 투영 TS 짝 · 구조물 따라감 py · ts 짝 = common_util_route_follow(정수 m · 구간 끝 밂 · 0 ~ 연장 자름 · 20 m 넘으면 멀어짐 표시 · 상수 STRUCTURE_ROUTE_DRIFT_MAX_M) - 구조물 x · y 저장(저장 때 노선 위 자리 · 자리가 그대로면 옛 좌표 지킴 · 옛 자료는 읽을 때 채움) · 노선 [확인] 때 구조물 정본이 좌표를 따라감 - 노선 바뀜 때 계획고 옮김 = 바뀐 구간만 새 지반 첫 계획선 · 앞 그대로 · 뒤 길이 차만큼 밂(B05_Profile_Engine_Grade_Carry.py ↔ logic/B05_Profile_Grade_Carry.ts) - 거울 시험 test_344_2_route_pairs(+ helper cjs) · buildCurves · evaluateAt 내보냄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -19,6 +19,8 @@
|
||||
|
||||
짝: 시설 속성 붙이기 `carry_indices` ↔ 화면 `B05_Profile/B05_Profile_UI_Drainage_Facility.ts`
|
||||
`carryIndices` — 같은 규칙(허용 오차 안 · 한 원본 한 번) · 거울 시험 test_b05_facility_carry.
|
||||
짝: 관 자리 투영 `project_pipe_points` ↔ TS `common_util/common_util_route_follow.ts`
|
||||
`projectPipePoints`(344-2 · 노선 바뀜 때 관이 좌표를 따라감) · 거울 시험 test_344_2_route_pairs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"""노선이 바뀔 때 구조물이 땅 위 자리(좌표)를 따라감 — 누가거리를 다시 매김(344-2).
|
||||
|
||||
사용자 확정(10-16 · 341 결정 2): 관 · 구조물의 자리 정본 = 좌표(x · y) · 누가거리는 그 자리를 지금
|
||||
노선에서 읽은 값. 노선이 바뀌면 좌표를 새 노선에 투영해 누가거리를 다시 매기고(정수 m · 234-1),
|
||||
새 노선에서 `STRUCTURE_ROUTE_DRIFT_MAX_M` 넘게 멀면 「노선에서 멀어짐」 표시만(버리지 않음).
|
||||
|
||||
- `fill_structure_coordinates` — 저장 때 노선 위 기준 자리로 x · y 채움(자리가 그대로면 옛 좌표
|
||||
지킴) · 옛 자료는 읽을 때 없는 것만 채움.
|
||||
- `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리.
|
||||
|
||||
짝 = TS `common_util/common_util_route_follow.ts` `followStructures`(구조물) · `projectPipePoints`
|
||||
(관 — 파이썬 본체 = `common_util_drainage_pipes.project_pipe_points`) · 거울 시험 =
|
||||
`resources/tester/test_344_2_route_pairs.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
from shapely.geometry import LineString, Point
|
||||
|
||||
from common_util.common_util_structure_station import snap_structure_station
|
||||
from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M
|
||||
|
||||
_POSITION_KEYS = ("chainage_m", "start_m", "end_m")
|
||||
|
||||
|
||||
def _line(xy: Sequence[Sequence[float]]) -> LineString | None:
|
||||
points = [(float(point[0]), float(point[1])) for point in xy]
|
||||
if len(points) < 2:
|
||||
return None
|
||||
line = LineString(points)
|
||||
return line if line.length > 0 else None
|
||||
|
||||
|
||||
def _positions(item: Any) -> tuple[Any, ...]:
|
||||
return tuple(getattr(item, key, None) for key in _POSITION_KEYS)
|
||||
|
||||
|
||||
def fill_structure_coordinates(
|
||||
items: Iterable[Any],
|
||||
xy: Sequence[Sequence[float]],
|
||||
*,
|
||||
previous: dict[str, Any] | None = None,
|
||||
only_missing: bool = False,
|
||||
) -> int:
|
||||
"""구조물 x · y 를 노선 위 기준 자리(`chainage_m`)로 채운다(제자리) · 채운 수.
|
||||
|
||||
`previous` = 저장 전 정본 {structure_id: 항목} — 자리(누가거리 셋)가 그대로면 옛 좌표를 지킨다
|
||||
(노선이 바뀌어 멀어진 땅 자리를 다음 저장이 노선 위로 덮지 않게). `only_missing` = 읽을 때.
|
||||
"""
|
||||
line = _line(xy)
|
||||
if line is None:
|
||||
return 0
|
||||
filled = 0
|
||||
for item in items:
|
||||
anchor = getattr(item, "chainage_m", None)
|
||||
if anchor is None:
|
||||
continue
|
||||
has_xy = getattr(item, "x", None) is not None and getattr(item, "y", None) is not None
|
||||
if only_missing and has_xy:
|
||||
continue
|
||||
old = (previous or {}).get(getattr(item, "structure_id", None) or "")
|
||||
if old is not None and _positions(old) == _positions(item) and old.x is not None:
|
||||
item.x, item.y = old.x, old.y
|
||||
continue
|
||||
point = line.interpolate(float(anchor))
|
||||
item.x, item.y = round(float(point.x), 4), round(float(point.y), 4)
|
||||
filled += 1
|
||||
return filled
|
||||
|
||||
|
||||
def follow_structures(
|
||||
items: Iterable[Any],
|
||||
xy: Sequence[Sequence[float]],
|
||||
max_drift_m: float = STRUCTURE_ROUTE_DRIFT_MAX_M,
|
||||
) -> list[tuple[float | None, bool]]:
|
||||
"""좌표를 새 노선에 투영해 누가거리를 다시 매긴다(제자리) → 항목마다 (노선과 거리, 멀어짐).
|
||||
|
||||
기준 자리 = 투영 → 정수 m(노선 끝 안) · 구간 끝(`start_m` · `end_m`)은 같은 만큼 밀고 0 ~ 연장
|
||||
안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None).
|
||||
"""
|
||||
items = list(items)
|
||||
line = _line(xy)
|
||||
if line is None:
|
||||
return [(None, False)] * len(items)
|
||||
length = float(line.length)
|
||||
result: list[tuple[float | None, bool]] = []
|
||||
for item in items:
|
||||
x, y = getattr(item, "x", None), getattr(item, "y", None)
|
||||
anchor = getattr(item, "chainage_m", None)
|
||||
if x is None or y is None or anchor is None:
|
||||
result.append((None, False))
|
||||
continue
|
||||
point = Point(float(x), float(y))
|
||||
shift = snap_structure_station(float(line.project(point)), [length]) - float(anchor)
|
||||
for key in _POSITION_KEYS:
|
||||
value = getattr(item, key, None)
|
||||
if value is not None:
|
||||
setattr(item, key, min(max(float(value) + shift, 0.0), length))
|
||||
drift = float(line.distance(point))
|
||||
result.append((drift, drift > max_drift_m))
|
||||
return result
|
||||
@@ -0,0 +1,116 @@
|
||||
/* =============================================================================
|
||||
* 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<XY>,
|
||||
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<P extends FollowingPoint>(
|
||||
points: ReadonlyArray<P>,
|
||||
line: ReadonlyArray<XY>,
|
||||
): 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<S extends FollowingStructure>(
|
||||
items: ReadonlyArray<S>,
|
||||
line: ReadonlyArray<XY>,
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user