Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_Edit.py
T

170 lines
7.2 KiB
Python

"""D 예정노선 꺾임점 고침 [저장] 정본(요구 363-1 · PLAN 543-3).
- 받는 것 = 연도별 마디 · 곡선 켬끔 · R(화면이 그은 선은 안 받음)
- 선 = B05 편집 갈래 `node_polyline`(Node `route.polyline` = 화면 `buildEditedPolyline` 같은 TS) ·
못 돌림 = 파이썬 짝 `build_planned_polyline(simplify=False)`
- 지표 = 엔진과 같은 `route_metrics` → `hard_limits` → `with_station_misses`(연도 지형 · 계류)
- 처음 · 끝 마디 = 연도 경계 지점(1 mm) · 고친 갈래 · 분석 지문이 지금과 같아야 함
계산 자리 = ① 파이썬 · TS 짝 — 화면 `logic/D03_PlanAnalysis_Edit.ts`(조작 중 즉시) ↔
여기([저장] 정본) · 거울 시험 `resources/tester/test_543_plan_edit.py`.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from B05_Profile.B05_Profile_Engine_RouteInitial import (
ROAD_WIDTH_M,
STREAM_OFFSET_M,
load_terrain_and_streams,
)
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import route_metrics
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import hard_limits
from B05_Profile.B05_Profile_Engine_RouteInitial_Station import with_station_misses
from B05_Profile.B05_Profile_Route_Polyline_Write import node_polyline
from common_util.common_util_route_polyline import PlannedPolyline, build_planned_polyline
from D03_PlanAnalysis import D03_PlanAnalysis_Analyze as analyze
# 끝 마디 = 지점 판정(m) — 짝: `D02_PlanBase_Router.SAME_M`
SAME_M = 1e-3
INPUT_KEYS = ("base_mode", "base_fingerprint", "nodes", "curve_flags", "radii")
class EditError(ValueError):
"""고침을 받을 수 없음 — 까닭 그대로 화면에."""
def edited_line(edit: dict[str, Any], min_radius_m: float) -> PlannedPolyline:
"""마디 · 켬끔 · R → 선(화면과 같은 편집 갈래)."""
points = [(float(x), float(y)) for x, y in edit["nodes"]]
flags = [bool(on) for on in edit["curve_flags"]]
radii = [None if r is None else float(r) for r in edit["radii"]]
built = node_polyline(points, min_radius_m, flags, radii, None, None)
if built is not None:
return built[0]
return build_planned_polyline(
points, min_radius_m=min_radius_m, simplify=False, curve_flags=flags, radii=radii
)
def check_edit(edit: dict[str, Any], result: dict[str, Any], mode: str) -> None:
"""고친 갈래 · 분석 지문 · 칸 수 · 끝 마디 = 지점."""
year = int(result["year"])
if edit["base_mode"] != mode:
raise EditError(f"{year}년 고침이 고른 갈래와 다릅니다.")
if edit["base_fingerprint"] != result.get("fingerprint"):
raise EditError(f"{year}년 분석이 바뀌어 고침이 낡았습니다 — 다시 고치세요.")
nodes = edit["nodes"]
if len(nodes) < 2 or not (len(nodes) == len(edit["curve_flags"]) == len(edit["radii"])):
raise EditError(f"{year}년 고침 마디 꼴이 다릅니다.")
if any(r is not None and not float(r) > 0 for r in edit["radii"]):
raise EditError(f"{year}년 R 은 0 보다 커야 합니다.")
for got, want in ((nodes[0], result["start"]), (nodes[-1], result["end"])):
if max(abs(float(a) - float(b)) for a, b in zip(got, want)) > SAME_M:
raise EditError(f"{year}년 처음 · 끝 마디가 연도 경계 지점과 다릅니다.")
def edit_metrics(
line: PlannedPolyline, result: dict[str, Any], terrain: Any, streams: list
) -> dict[str, Any]:
"""엔진과 같은 잼 — 원지반 기울기는 참고(못 지킨 수에서 뺌) · 측점 밖 곡선 더함."""
criteria = result["criteria"]
metrics = route_metrics(
line.vertices,
[tuple(result["start"]), tuple(result["end"])],
terrain,
streams,
line.curves,
max_grade=float(criteria["max_grade_pct"]) / 100.0,
min_radius_m=float(criteria["min_radius_m"]),
stream_offset_m=STREAM_OFFSET_M,
road_width_m=ROAD_WIDTH_M,
bound_pct=None,
)
return with_station_misses(hard_limits(metrics), line)
def saved_edit(route_json: Any) -> dict[str, Any] | None:
"""저장된 고침(route_json `edit` + 그때 선 · 지표) — 없으면 None."""
try:
value = json.loads(route_json) if isinstance(route_json, str) else route_json
except ValueError:
return None
edit = value.get("edit") if isinstance(value, dict) else None
if not isinstance(edit, dict) or not all(key in edit for key in INPUT_KEYS):
return None
return {
**{key: edit[key] for key in INPUT_KEYS},
"planned": value.get("planned"),
"nodes_info": value.get("nodes"),
"curves": value.get("curves"),
"metrics": value.get("metrics"),
}
def same_inputs(a: dict[str, Any] | None, b: dict[str, Any] | None) -> bool:
"""같은 고침 입력인가(1 mm) — 같으면 저장된 선 · 지표를 다시 씀."""
if not a or not b:
return False
def near(x: Any, y: Any) -> bool:
return (
x is None
and y is None
or (x is not None and y is not None and abs(float(x) - float(y)) < 1e-3)
)
return (
a["base_mode"] == b["base_mode"]
and a["base_fingerprint"] == b["base_fingerprint"]
and len(a["nodes"]) == len(b["nodes"])
and all(near(p[0], q[0]) and near(p[1], q[1]) for p, q in zip(a["nodes"], b["nodes"]))
and [bool(v) for v in a["curve_flags"]] == [bool(v) for v in b["curve_flags"]]
and all(near(x, y) for x, y in zip(a["radii"], b["radii"]))
)
def build_edits(
project_root: Path,
results: list[dict[str, Any]],
rows: list[dict[str, Any]],
edits: dict[int, dict[str, Any]],
saved: dict[int, dict[str, Any] | None],
loader: analyze.Loader = load_terrain_and_streams,
) -> dict[int, dict[str, Any]]:
"""연도 → 고친 노선 `{mode, planned, nodes, curves, metrics, edit}` — 저장된 고침과 입력이
같으면 그 선 · 지표 그대로(지형 안 읽음)."""
out: dict[int, dict[str, Any]] = {}
for result, row in zip(results, rows):
year = int(result["year"])
edit = edits.get(year)
if edit is None:
continue
check_edit(edit, result, row["mode"])
inputs = {key: edit[key] for key in INPUT_KEYS}
old = saved.get(year)
if old is not None and old.get("metrics") and same_inputs(inputs, old):
out[year] = {
"mode": row["mode"],
"planned": old["planned"],
"nodes": old["nodes_info"],
"curves": old["curves"],
"metrics": old["metrics"],
"edit": inputs,
}
continue
line = edited_line(inputs, float(result["criteria"]["min_radius_m"]))
terrain, streams = analyze._load(project_root, year, loader)
out[year] = {
"mode": row["mode"],
"planned": [[round(x, 4), round(y, 4)] for x, y in line.vertices],
"nodes": [node.as_dict() for node in line.nodes],
"curves": [curve.as_dict() for curve in line.curves],
"metrics": edit_metrics(line, result, terrain, streams),
"edit": inputs,
}
return out