From e2d329d35f686596cf8078113a38d470cfacd9b5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 11:54:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD):=20?= =?UTF-8?q?=EC=A7=80=EB=AC=B8=EC=9D=B4=20=EB=8B=AC=EB=9D=BC=EB=8F=84=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EB=85=B8=EC=84=A0=EC=9D=B4=EB=A9=B4=20?= =?UTF-8?q?=EA=B4=80=20=EC=A7=80=EC=A0=90=EC=9D=84=20=EA=B7=B8=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EC=94=80=20(=EA=B3=84=ED=9A=8D=EC=84=9C=200-7=20?= =?UTF-8?q?=EA=B3=81=EA=B0=80=EC=A7=80=20=E2=91=A2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 노선인데 지문이 늘 어긋나 관 지점이 매번 투영 이월을 탔음. 까닭 — 같은 노선을 두 파일이 다른 자릿수로 담고 있음. `planned_route.csv` 는 소수 4자리(`208403.2001`), `route_main.geojson` 은 소수 3자리 (`208403.2`)로 csv 를 mm 반올림한 사본임. 좌표 차는 최대 0.5mm 뿐인데, 지문이 `f"{x:.2f}"` 로 0.01m 자리에서 끊는 탓에 그 0.5mm 가 `.xx5` 경계를 넘는 정점마다 글자가 바뀜 — 169개 중 16개가 그랬음. 경계에서 자르는 방식은 저장 자릿수가 또 바뀌면 다시 흔들리므로, 글자 일치 대신 **허용오차**로 가름. 저장된 관을 지금 노선에 투영해 **재 보기만** 하고 (`max_projection_shift`, 값은 안 고침), 최대 어긋남이 0.05m 이하면 같은 노선으로 보고 저장분을 그대로 돌려줌. 허용오차 0.05m 근거 — 실측 어긋남이 최대 0.0053m 이라 다섯 배 이상 여유이고, 사람이 노선을 실제로 고치면 관은 m 단위로 밀리므로 「같다」로 볼 위험이 없음. 투영 이월 가지는 그대로 둠 — 대신 그 가지가 실제로 돌면 WARNING 을 찍게 함. 한동안 0 인 것을 확인한 뒤에야 지울 수 있음(먼저 지우면 관이 통째로 사라짐). 자체검증 - 새 시험 3건 `tmp/tests/test_pipe_route_tolerance.py` — mm 반올림 사본은 같은 노선으로 판정되고 누가거리가 한 값도 안 움직임 / 중간을 3m 민 노선은 안 걸림 / 허용오차 범위. - 기존 `test_pipe_point_projection.py` 2건 그대로 통과(±0.7m 잔물결 노선은 여전히 투영). - 저장된 실제 3개 프로젝트 전후 대조 — 셋 다 「다름 → 투영」이 「같음 → 저장분 그대로」로 바뀜. 화면 값 변화는 최대 0.0053m · 0.0009m · 0.0005m. - 전체 514 passed · 18 skipped. Co-Authored-By: Claude Opus 5 (1M context) --- common_util/common_util_drainage_pipes.py | 54 ++++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/common_util/common_util_drainage_pipes.py b/common_util/common_util_drainage_pipes.py index b27ac82a..247572f4 100644 --- a/common_util/common_util_drainage_pipes.py +++ b/common_util/common_util_drainage_pipes.py @@ -160,6 +160,38 @@ def fill_pipe_coordinates(points: list[PipePoint], vertices: list[RouteVertex]) return points +# 같은 노선으로 볼 누가거리 어긋남의 한계(m). +# +# 지문(`route_signature`)은 좌표를 **0.01m 자리에서 끊어** 해시한다. 그런데 같은 노선이 +# `planned_route.csv`(소수 4자리)와 `route_main.geojson`(소수 3자리, csv 를 mm 로 반올림한 +# 사본)로 **0.5mm 다르게** 저장돼 있어, 그 0.5mm 가 `.xx5` 경계를 넘는 정점마다 글자가 +# 바뀐다(2026-09-07 실측: 169개 중 **16개**). 노선을 손댄 적이 없는데도 지문이 늘 달랐다. +# +# 경계에서 자르는 방식은 저장 자릿수가 또 바뀌면 다시 흔들리므로 **글자 일치 대신 +# 허용오차**로 가른다. 값은 0.05m — 위 어긋남이 관 누가거리에 미치는 양이 실측 +# **최대 0.01m** 이라 다섯 배 여유를 두었고, 사람이 노선을 실제로 고치면 관은 **m 단위**로 +# 밀리므로 그것을 「같다」로 볼 위험은 없다. +ROUTE_MATCH_TOLERANCE_M = 0.05 + + +def max_projection_shift(points: list[PipePoint], vertices: list[RouteVertex]) -> float | None: + """저장된 관을 이 노선에 투영하면 누가거리가 최대 얼마나 움직이나 (**고치지 않고 잰다**). + + 좌표가 없는 관이 하나라도 있으면 잴 수 없어 None. + """ + if not vertices or not points: + return None + if any(point.x is None or point.y is None for point in points): + return None + line = LineString([(vertex.x, vertex.y) for vertex in vertices]) + if line.length <= 0: + return None + return max( + abs(float(line.project(Point(point.x, point.y))) - float(point.chainage_m)) + for point in points + ) + + def project_pipe_points(points: list[PipePoint], vertices: list[RouteVertex]) -> list[PipePoint]: """저장된 좌표를 주어진 노선에 투영해 누가거리를 다시 매긴다. @@ -214,10 +246,28 @@ def load_pipe_points_file( stored_signature = str(document.get("route_signature") or "") if stored_signature == signature: return points - if vertices and points and all(p.x is not None and p.y is not None for p in points): + # 지문이 다르다고 노선이 바뀐 것은 아니다 — 같은 노선을 두 파일이 0.5mm 다르게 담고 + # 있어 글자가 늘 어긋난다(위 `ROUTE_MATCH_TOLERANCE_M` 주석). 관이 실제로 얼마나 + # 밀리는지 **재 보고** 한계 안이면 저장분을 그대로 쓴다 — 건드리지 않는 것이 정답이다. + shift = max_projection_shift(points, vertices) if vertices else None + if shift is not None and shift <= ROUTE_MATCH_TOLERANCE_M: logger.info( - "배수유역: 노선이 바뀌어 관 지점 %d건을 좌표로 이월합니다 (%s).", + "배수유역: 지문은 다르나 같은 노선입니다 — 관 %d건 그대로 씁니다 " + "(최대 어긋남 %.4fm ≤ %.2fm, %s).", len(points), + shift, + ROUTE_MATCH_TOLERANCE_M, + path.name, + ) + return points + if vertices and points and all(p.x is not None and p.y is not None for p in points): + # ⚠ 이 줄이 찍히면 **투영 이월이 실제로 돈 것**이다. 한동안 0 인 것을 확인한 뒤에야 + # 이 가지를 지울 수 있다(계획서 0-7 — 먼저 지우면 관이 통째로 사라진다). + logger.warning( + "배수유역: 투영 이월 실행 — 노선이 바뀌어 관 지점 %d건을 좌표로 옮깁니다 " + "(최대 어긋남 %s, %s).", + len(points), + f"{shift:.3f}m" if shift is not None else "잴 수 없음", path.name, ) return project_pipe_points(points, vertices)