fix(493-4 관 저장 자리 = 화면 자리): 관 PUT 이 땅 위 좌표를 버려 노선 위 측점 자리로 덮던 것 · GET 관 좌표 = 정본 땅 위 자리
- 원인 = 관 PUT 이 세부유역 계산을 거쳐 속성만 되붙임(carry_facility_attributes · x · y 빠짐) → 저장이 노선 위 측점 자리로 채움 + GET 관 x · y = 노선 위 자리라 [저장] 다시 얹기가 그 자리를 베낌 · 검증_sub2 재현 0.62 m(검증_sub1 1.27 m) - carry_facility_attributes 가 x · y 도 되붙임 · save_pipe_points = 좌표가 그 누가거리를 낳으면 땅 위 자리 그대로 · 누가거리만 옮긴 낡은 좌표는 노선 위(구조물 fill_structure_coordinates · TS placeOnLine 같은 규칙) - GET 관 지점 x · y = 정본 좌표(없으면 노선 위 자리) · 지도 lonlat 은 노선 위 그대로 - 거울 시험 test_493_3_follow_screen_save 에 관 x · y(끌기 뒤 화면 = 관 PUT 뒤 정본 · 두 번 고쳐도 그대로 · 누가거리만 옮긴 관 = 노선 위 · 고치기 전 대조 0.2 m 넘게 덮임) · test_428 GET 좌표 글 맞춤 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -366,9 +366,10 @@ def _raw_payload(
|
||||
**point.as_dict(),
|
||||
"chainage_m": round(pipe.chainage_m, 2),
|
||||
"lonlat": list(to_lonlat(pipe.x, pipe.y)),
|
||||
# 노선 위 자리(사업지 CRS) — 화면이 따라가기만 한 관을 가림(rebasePipeDraft · 428)
|
||||
"x": round(float(pipe.x), 3),
|
||||
"y": round(float(pipe.y), 3),
|
||||
# 땅 위 자리(사업지 CRS · 정본 좌표 · 없으면 노선 위 자리) — 화면이 따라가기만 한 관을
|
||||
# 가림(rebasePipeDraft · 428) · 노선 위 자리를 주면 [저장] 다시 얹기가 정본을 덮음(493-4)
|
||||
"x": round(float(pipe.x if point.x is None else point.x), 3),
|
||||
"y": round(float(pipe.y if point.y is None else point.y), 3),
|
||||
"source": pipe.reason,
|
||||
}
|
||||
for pipe, point in zip(detail.pipes, points)
|
||||
|
||||
@@ -38,6 +38,7 @@ from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_route_geometry import RouteVertex, interpolate_vertex
|
||||
from common_util.common_util_structure_station import (
|
||||
SAME_STATION_M,
|
||||
route_general_stations,
|
||||
snap_structure_station,
|
||||
)
|
||||
@@ -269,6 +270,13 @@ def max_projection_shift(points: list[PipePoint], vertices: list[RouteVertex]) -
|
||||
)
|
||||
|
||||
|
||||
def _holds_station(point: PipePoint, vertices: list[RouteVertex]) -> bool:
|
||||
"""좌표를 이 노선에 따라가도 누가거리가 그대로인가(TS `holdsStation` 짝)."""
|
||||
probe = PipePoint(chainage_m=float(point.chainage_m), x=point.x, y=point.y)
|
||||
project_pipe_points([probe], vertices)
|
||||
return abs(probe.chainage_m - float(point.chainage_m)) <= SAME_STATION_M
|
||||
|
||||
|
||||
def project_pipe_points(points: list[PipePoint], vertices: list[RouteVertex]) -> list[PipePoint]:
|
||||
"""저장된 좌표를 주어진 노선에 투영해 누가거리를 다시 매긴다.
|
||||
|
||||
@@ -482,6 +490,8 @@ def carry_facility_attributes(base: list[PipePoint], reference: list[PipePoint])
|
||||
point.summary_item = ref.summary_item
|
||||
point.group_items = ref.group_items
|
||||
point.id = ref.id
|
||||
# 땅 위 자리도 — 버리면 저장이 노선 위 측점 자리로 채워 정본 좌표를 덮음(493-4)
|
||||
point.x, point.y = ref.x, ref.y
|
||||
return ensure_pipe_ids(base)
|
||||
|
||||
|
||||
@@ -494,9 +504,14 @@ def save_pipe_points(
|
||||
"""관 지점을 정본 파일에 쓴다. 저장된 개수를 돌려준다.
|
||||
|
||||
`vertices`를 주면 좌표가 빈 관을 그 노선 위 좌표로 채워 둔다 — 다음에 다른 선으로
|
||||
읽어도 그 자리에 되놓을 수 있다.
|
||||
읽어도 그 자리에 되놓을 수 있다. 좌표가 그 누가거리를 안 낳으면(누가거리만 옮긴 낡은 좌표)
|
||||
노선 위로 · 낳으면 땅 위 자리 그대로(493-4 · 구조물 `fill_structure_coordinates` · TS
|
||||
`placeOnLine` 같은 규칙).
|
||||
"""
|
||||
if vertices:
|
||||
for point in points:
|
||||
if point.x is not None and point.y is not None and not _holds_station(point, vertices):
|
||||
point.x = point.y = None
|
||||
fill_pipe_coordinates(points, vertices)
|
||||
ensure_pipe_ids(points)
|
||||
path = pipe_points_path(stored_path)
|
||||
|
||||
@@ -229,4 +229,6 @@ def test_관_PUT_끝_측점_다시_세움_GET_좌표():
|
||||
stand = inspect.getsource(basins._stand_pipe_stations)
|
||||
assert "ensure_structure_stations(project_id)" in stand and "except Exception" in stand
|
||||
raw = inspect.getsource(basins._raw_payload)
|
||||
assert '"x": round(float(pipe.x), 3)' in raw and '"y": round(float(pipe.y), 3)' in raw
|
||||
# GET 좌표 = 정본 땅 위 자리(없으면 노선 위 자리 · 493-4)
|
||||
assert '"x": round(float(pipe.x if point.x is None else point.x), 3)' in raw
|
||||
assert '"y": round(float(pipe.y if point.y is None else point.y), 3)' in raw
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
`B05_Profile_Plus_Stations.followed_plus_rows` 와 같은 셈). 규칙(요구 290) = 관 · 구조물 · 추가
|
||||
측점 = 땅 위 자리(x · y)에 가장 가까운 새 노선 자리.
|
||||
|
||||
493-4 = 관 땅 위 자리(x · y) 자체 — 화면 관 좌표 = 관 PUT(`put_pipe_points` · 속성 되붙임
|
||||
`carry_facility_attributes` → `save_pipe_points`) 뒤 정본 좌표 · 두 번 고쳐 저장해도 그대로 · 누가거리만
|
||||
옮긴 관 = 노선 위.
|
||||
|
||||
경우 = 직선 · 곡선 R · 교점 더함 · 지움 · 노선 앞 교점 옮김(여러 걸음 · 오가며).
|
||||
노선 = 서버 `build_planned_polyline`(브라우저 `draftLine` 짝 ·
|
||||
test_route_polyline_browser_mirror.py) · 4자리(계획노선 csv · `draftLine` `xy4`).
|
||||
@@ -254,3 +258,91 @@ def test_고치기_전_추가_측점은_걸음마다_밀림(tmp_path):
|
||||
bad = _gaps(_browser(lines, tmp_path, old_plus=True), _server(lines), lines[-1])
|
||||
assert bad and all(name == "추가 측점" for name, _, _ in bad)
|
||||
assert max(abs(got - want) for _, got, want in bad) >= 2
|
||||
|
||||
|
||||
# ── 493-4 관 땅 위 자리(x · y) — [저장] 관 PUT(세부유역 계산 → 속성 되붙임 → 저장)이 화면 좌표 그대로 ──
|
||||
|
||||
|
||||
def _put_pipes(draft: list[dict], new, tmp_path, monkeypatch, carry_xy: bool = True) -> dict:
|
||||
"""관 PUT 길(`put_pipe_points`) — 본문 → 계산기가 돌려준 누가거리만의 관(`_rebuilt` 꼴) → 속성
|
||||
되붙임 → 정본 저장(새 노선) → id → (x, y). `carry_xy=False` = 고치기 전(되붙임이 좌표를 버림)."""
|
||||
from common_util import common_util_storage as storage
|
||||
from common_util.common_util_drainage_pipes import (
|
||||
carry_facility_attributes,
|
||||
parse_pipe_points,
|
||||
pipe_points_path,
|
||||
save_pipe_points,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(storage, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
||||
(tmp_path / "storage" / "p").mkdir(parents=True)
|
||||
points = parse_pipe_points(copy.deepcopy(draft))
|
||||
rebuilt = [PipePoint(chainage_m=round(p.chainage_m, 2), source=p.source) for p in points]
|
||||
carried = carry_facility_attributes(rebuilt, points)
|
||||
if not carry_xy:
|
||||
for point in carried:
|
||||
point.x = point.y = None
|
||||
save_pipe_points("storage/p", "sig", carried, vertices_of(new))
|
||||
saved = json.loads(pipe_points_path("storage/p").read_text(encoding="utf-8"))["points"]
|
||||
return {p["id"]: (p["x"], p["y"]) for p in saved}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", list(CASES))
|
||||
def test_관_저장_좌표_화면_좌표_같음(case, tmp_path, monkeypatch):
|
||||
"""끌기 뒤 화면 관 땅 위 자리 = [저장](관 PUT) 뒤 정본 좌표(0.01 m 안)."""
|
||||
lines = CASES[case]
|
||||
screen = _browser(lines, tmp_path)["pipes"]
|
||||
saved = _put_pipes(screen, lines[-1], tmp_path, monkeypatch)
|
||||
bad = [
|
||||
(p["id"], round(math.hypot(p["x"] - saved[p["id"]][0], p["y"] - saved[p["id"]][1]), 3))
|
||||
for p in screen
|
||||
if math.hypot(p["x"] - saved[p["id"]][0], p["y"] - saved[p["id"]][1]) > 0.01
|
||||
]
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_관_두_번_고쳐_저장해도_자리_그대로(tmp_path, monkeypatch):
|
||||
"""[저장] 정본 좌표에서 다시 끌어 [저장] — 땅 위 자리가 흘러가지 않음(처음 정본 0.01 m 안)."""
|
||||
first, second = CASES["앞_교점_옮김"], CASES["곡선_교점_옮김"]
|
||||
(tmp_path / "a").mkdir()
|
||||
screen = _browser(first, tmp_path / "a")["pipes"]
|
||||
saved = _put_pipes(screen, first[-1], tmp_path / "a", monkeypatch)
|
||||
again = [{**p, "x": saved[p["id"]][0], "y": saved[p["id"]][1]} for p in screen]
|
||||
lines = [first[-1], *second[1:]]
|
||||
source = tmp_path / "b"
|
||||
source.mkdir()
|
||||
(source / "in.json").write_text(
|
||||
json.dumps({"lines": lines, "pipes": again, "structures": [], "plus": [], "interval": 20}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", str(HERE / "helper_493_3_follow_steps.cjs"), str(source / "in.json")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
cwd=str(ROOT),
|
||||
check=False,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
final = _put_pipes(json.loads(proc.stdout)["pipes"], lines[-1], tmp_path / "b", monkeypatch)
|
||||
for pipe in PIPES:
|
||||
x, y = final[pipe["id"]]
|
||||
assert math.hypot(x - pipe["x"], y - pipe["y"]) <= 0.01, pipe["id"]
|
||||
|
||||
|
||||
def test_고치기_전_관_저장은_노선_위로_덮음(tmp_path, monkeypatch):
|
||||
"""대조 — 되붙임이 좌표를 버리던 셈(493-4 전)은 노선 옆 관(0.2 ~ 0.4 m)을 노선 위 측점 자리로 덮음."""
|
||||
lines = CASES["앞_교점_옮김"]
|
||||
screen = _browser(lines, tmp_path)["pipes"]
|
||||
saved = _put_pipes(screen, lines[-1], tmp_path, monkeypatch, carry_xy=False)
|
||||
gaps = [math.hypot(p["x"] - saved[p["id"]][0], p["y"] - saved[p["id"]][1]) for p in screen]
|
||||
assert max(gaps) >= 0.2
|
||||
|
||||
|
||||
def test_누가거리만_옮긴_관은_노선_위로(tmp_path, monkeypatch):
|
||||
"""배수유역 칸 등에서 누가거리만 옮긴 관(좌표 = 옛 자리) — 저장 = 새 누가거리의 노선 위 자리."""
|
||||
new = L0
|
||||
moved = {**PIPES[0], "chainage_m": 110.0}
|
||||
saved = _put_pipes([moved], new, tmp_path, monkeypatch)
|
||||
x, y = _at(new, 110.0)
|
||||
assert math.hypot(saved["p1"][0] - x, saved["p1"][1] - y) <= 0.01
|
||||
|
||||
Reference in New Issue
Block a user