fix(459 덧 노선 밖 시설): 시점 앞 · 종점 뒤 0.51 m 넘은 관 · 구조물 = 측점 계획 · 알약에서 뺌 · 좌표 그대로 · 노선 밖 알림
- 판정 routeOutside ↔ route_outside(TS ↔ 파이썬 · 끝 선분 연장으로 나간 거리 · 옆으로 먼 것은 아님) - 따라감(followStructures · projectPipePoints)이 outside 표시(파생값 · 구조물 저장 때 뺌) · planStations 가 뺌 - 서버는 측점 세울 때 노선 좌표로 그때그때(resolve_extra_stations xy · 부르는 곳 여섯) · 노선 저장 응답 pipes · structures.outside - 알림 「노선 밖 N 곳」 = [저장](design_save) · 초안 동안(followPlusDraft 가 follow 출력 통째 받음 · page 부름 인자만 바꿈 · 700줄 그대로) - 시험 test_459 노선 밖 하나 · 344_2 · 346_3 응답 기대값에 outside Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
지킴 · 보낸 좌표가 그 누가거리를 낳으면 살림 · 410) · 옛 자료는 읽을 때 없는 것만 채움.
|
||||
- `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리.
|
||||
|
||||
- `route_outside` — 노선 밖(시점 앞 · 종점 뒤 0.51 m 넘음) · 측점 세울 때 셈(459 · 정본에 안 씀).
|
||||
|
||||
짝 = TS `common_util/common_util_route_follow.ts` `followStructures`(구조물) · `projectPipePoints`
|
||||
(관 — 파이썬 본체 = `common_util_drainage_pipes.project_pipe_points`) · `placeOnLine`(좌표 낡음
|
||||
판정) · 거울 시험 = `resources/tester/test_344_2_route_pairs.py` · `test_410_follow_xy.py`.
|
||||
@@ -15,6 +17,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any, Iterable, Sequence
|
||||
|
||||
from shapely.geometry import LineString, Point
|
||||
@@ -28,6 +31,8 @@ from common_util.common_util_structure_station import (
|
||||
from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M
|
||||
|
||||
_POSITION_KEYS = ("chainage_m", "start_m", "end_m")
|
||||
#: 노선 밖 판정(m) — 투영이 시점 앞 · 종점 뒤로 이만큼 넘으면 측점 안 세움(459 · TS 짝).
|
||||
ROUTE_OUTSIDE_M = 0.51
|
||||
|
||||
|
||||
def _line(xy: Sequence[Sequence[float]]) -> LineString | None:
|
||||
@@ -38,6 +43,25 @@ def _line(xy: Sequence[Sequence[float]]) -> LineString | None:
|
||||
return line if line.length > 0 else None
|
||||
|
||||
|
||||
def route_outside(
|
||||
x: float | None, y: float | None, xy: Sequence[Sequence[float]], tol_m: float = ROUTE_OUTSIDE_M
|
||||
) -> bool:
|
||||
"""노선 밖인가(459) — 가장 가까운 자리가 시점 · 종점이고 그 끝 선분 연장으로 `tol_m` 넘게 나감
|
||||
(옆으로 먼 것은 아님) · 좌표 없으면 거짓. TS `routeOutside` 짝 · 측점 계획에서 뺌."""
|
||||
line = _line(xy)
|
||||
if line is None or x is None or y is None:
|
||||
return False
|
||||
at = float(line.project(Point(float(x), float(y))))
|
||||
if 0.0 < at < float(line.length):
|
||||
return False
|
||||
coords = list(line.coords)
|
||||
(ax, ay), (bx, by) = (coords[0], coords[1]) if at <= 0.0 else (coords[-1], coords[-2])
|
||||
span = math.hypot(bx - ax, by - ay)
|
||||
if span <= 0.0:
|
||||
return False
|
||||
return -((float(x) - ax) * (bx - ax) + (float(y) - ay) * (by - ay)) / span > tol_m
|
||||
|
||||
|
||||
def _positions(item: Any) -> tuple[Any, ...]:
|
||||
return tuple(getattr(item, key, None) for key in _POSITION_KEYS)
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
* 전 자리로 · 노선을 저장한 [저장]이 서버가 따라 옮긴 관 위에 초안을 얹음(349-0u).
|
||||
* - `rebaseStructureDraft` 도 TS 만 — 구조물 초안을 서버가 따라 옮긴 정본 위에(459 · 관 짝).
|
||||
* - `followPlusStations` ↔ 파이썬 `B05_Profile_Plus_Stations.follow_plus_stations`(459 · 노선 초안 동안).
|
||||
* - `routeOutside` ↔ 파이썬 `route_outside`(459 · 노선 밖 = 측점 계획 · 알약에서 뺌 · 좌표 그대로) — 따라감이
|
||||
* `outside: true` 를 붙임(파생값 · 저장 때 뺌 · 서버는 측점 세울 때 다시 셈).
|
||||
* 거울 시험 = resources/tester/test_344_2_route_pairs.py.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -28,6 +30,9 @@ import {
|
||||
/** 「노선에서 멀어짐」 거리(m) — 파이썬 `config_system.STRUCTURE_ROUTE_DRIFT_MAX_M` 짝(거울 시험이 대조). */
|
||||
export const STRUCTURE_ROUTE_DRIFT_MAX_M = 20;
|
||||
|
||||
/** 노선 밖 판정(m) — 투영이 시점 앞 · 종점 뒤로 이만큼 넘음(459 · 파이썬 `ROUTE_OUTSIDE_M` · 458 주인 측점 상한과 같은 값). */
|
||||
export const ROUTE_OUTSIDE_M = 0.51;
|
||||
|
||||
export interface XY {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -57,6 +62,33 @@ export function projectOnPolyline(
|
||||
return best && walked > 0 ? { ...best, length_m: walked } : null;
|
||||
}
|
||||
|
||||
/** 노선 밖인가(459) — 가장 가까운 자리가 시점 · 종점이고 그 끝 선분 연장으로 `tolM` 넘게 나감(옆으로 먼 것은 아님).
|
||||
* 파이썬 `route_outside` 짝. */
|
||||
export function routeOutside(
|
||||
line: ReadonlyArray<XY>,
|
||||
point: XY,
|
||||
tolM: number = ROUTE_OUTSIDE_M,
|
||||
): boolean {
|
||||
const hit = projectOnPolyline(line, point);
|
||||
if (!hit || (hit.chainage_m > 0 && hit.chainage_m < hit.length_m)) return false;
|
||||
const [a, b] =
|
||||
hit.chainage_m <= 0 ? [line[0], line[1]] : [line[line.length - 1], line[line.length - 2]];
|
||||
const span = Math.hypot(b.x - a.x, b.y - a.y);
|
||||
if (!(span > 0)) return false;
|
||||
return -((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / span > tolM;
|
||||
}
|
||||
|
||||
/** 노선 밖 표시를 다시 붙인 사본 — 좌표가 있고 노선 밖이면 `outside: true` · 아니면 칸 뺌(파생값 · 정본에 안 씀). */
|
||||
function markOutside<T extends { x?: number | null; y?: number | null }>(
|
||||
item: T,
|
||||
line: ReadonlyArray<XY>,
|
||||
): T {
|
||||
const { outside: _was, ...rest } = item as T & { outside?: boolean };
|
||||
return item.x != null && item.y != null && routeOutside(line, { x: item.x, y: item.y })
|
||||
? ({ ...rest, outside: true } as unknown as T)
|
||||
: (rest as T);
|
||||
}
|
||||
|
||||
/** 좌표가 그 누가거리를 낳는가 — `line` 에서 따라감(같은 선 = 제자리)이 누가거리를 안 옮김(410 · 파이썬
|
||||
* `fill_structure_coordinates` 같은 규칙). 아니면 좌표가 낡음(마커 · 방향키 · 폼으로 누가거리만 옮김). */
|
||||
const holdsStation = (
|
||||
@@ -232,12 +264,15 @@ export function projectPipePoints<P extends FollowingPoint>(
|
||||
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 } : {}),
|
||||
};
|
||||
return markOutside(
|
||||
{
|
||||
...point,
|
||||
chainage_m: moved,
|
||||
...(point.start_m != null ? { start_m: point.start_m + shift } : {}),
|
||||
...(point.end_m != null ? { end_m: point.end_m + shift } : {}),
|
||||
},
|
||||
line,
|
||||
);
|
||||
})
|
||||
.sort((a, b) => a.chainage_m - b.chainage_m);
|
||||
}
|
||||
@@ -271,12 +306,15 @@ export function followStructures<S extends FollowingStructure>(
|
||||
const shift = target - 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) } : {}),
|
||||
},
|
||||
item: markOutside(
|
||||
{
|
||||
...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) } : {}),
|
||||
},
|
||||
line,
|
||||
),
|
||||
drift_m: hit.distance_m,
|
||||
far: hit.distance_m > maxDriftM,
|
||||
};
|
||||
|
||||
@@ -200,11 +200,15 @@ export function structureStationMarks(
|
||||
export interface PlanPipe extends PlantingPipe {
|
||||
/** 관 세션 id — 없으면 자리로 지음(`pipe-{누가거리 2자리}`). */
|
||||
id?: string | null;
|
||||
/** 노선 밖(459 · 따라감이 붙이는 파생값) — 측점 계획에서 뺌. */
|
||||
outside?: boolean;
|
||||
options?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface PlanStructure extends PlantingStructure {
|
||||
structure_id?: string | null;
|
||||
/** 노선 밖(459 · 따라감이 붙이는 파생값) — 측점 계획에서 뺌. */
|
||||
outside?: boolean;
|
||||
options?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
@@ -249,13 +253,14 @@ export function planStations(
|
||||
const own: Mark[] = [
|
||||
...pipes.flatMap((pipe) => {
|
||||
const facility = pipe.facility ?? "pipe";
|
||||
if (!catalogRow(facility)) return [];
|
||||
if (!catalogRow(facility) || pipe.outside) return []; // 노선 밖(459) = 측점 안 세움
|
||||
const title = cardTitle(facility, pipe.options, defaults(facility));
|
||||
const owner = pipe.id ?? `pipe-${pipe.chainage_m.toFixed(2)}`;
|
||||
const blank = blankKeys(facility, pipe.options);
|
||||
return pipeStationMarks(pipe).map((m) => ({ ...m, title, owner, blank, pipe: true }));
|
||||
}),
|
||||
...structures.flatMap((structure) => {
|
||||
if (structure.outside) return []; // 노선 밖(459 · 따라감 표시)
|
||||
const marks = structureStationMarks(structure, byId.get(structure.type_id));
|
||||
const anchor = marks.find((m) => m.role === "anchor")?.chainage_m ?? 0;
|
||||
const title = cardTitle(structure.type_id, structure.options, defaults(structure.type_id));
|
||||
|
||||
Reference in New Issue
Block a user