170 lines
7.2 KiB
Python
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
|