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)