Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1
This commit is contained in:
@@ -413,7 +413,11 @@ async def run_redesign_chain(
|
||||
method=str(selection.get("method") or params.get("method") or "dtm"),
|
||||
smooth=bool(selection.get("smooth", params.get("smooth", False))),
|
||||
surface_model_id=surface_model_id,
|
||||
algorithm=str(params.get("algorithm") or "dijkstra"),
|
||||
# 사용자가 고친 계획노선을 **그대로** 쓴다 — 다시 풀면 탐색 제약(종단경사·
|
||||
# 최소곡선반지름·회피지역)에 걸려 체인이 통째로 멈춘다(2026-09-06 실측:
|
||||
# 「세그먼트 1 (BP → CP1) 경로 탐색 실패」로 종횡단만 옛 노선에 남았음).
|
||||
# PLAN 0-3 에서 자동탐색은 접기로 했고 노선은 사용자가 직접 고친다.
|
||||
algorithm="as_planned",
|
||||
bp=points["bp"],
|
||||
ep=points["ep"],
|
||||
cp=points.get("cp") or [],
|
||||
|
||||
@@ -106,7 +106,20 @@ def run_route_design(
|
||||
},
|
||||
options=options,
|
||||
)
|
||||
if algorithm == "ridge_valley":
|
||||
if algorithm == "as_planned":
|
||||
# 사용자가 고친 계획노선을 **그대로** 쓴다 — 다시 풀지 않는다(PLAN 0-3 자동탐색 접기).
|
||||
# 제어점 목록(bp·cp·ep)이 곧 노선이며, 표고만 지표면에서 뜬다.
|
||||
from B05_Profile.B05_Profile_Engine_AsPlanned import solve_as_planned
|
||||
|
||||
sequence = [points_data.get("bp")] + list(points_data.get("cp") or [])
|
||||
sequence.append(points_data.get("ep"))
|
||||
vertices = [
|
||||
(float(point["x"]), float(point["y"])) for point in sequence if isinstance(point, dict)
|
||||
]
|
||||
result = solve_as_planned(
|
||||
project_root, filter_key, smooth, vertices, options, method=method
|
||||
)
|
||||
elif algorithm == "ridge_valley":
|
||||
result = solve_ridge_valley_route(
|
||||
project_root, filter_key, smooth, points_data, options, method=method
|
||||
)
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""사용자가 고친 계획노선을 **그대로 쓰는** 길 — 다시 풀지 않는다.
|
||||
|
||||
왜 필요한가(2026-09-06 실측) — 노선 편집 [확인]이 재확정 체인을 타는데, 그 체인이
|
||||
`solve_route` 로 **BP·CP·EP 제어점 사이를 격자에서 다시 풀었다**. 사용자가 노드를 조금만
|
||||
비틀어도 탐색 제약에 걸려 체인이 통째로 멈췄다:
|
||||
|
||||
세그먼트 1 (BP → CP1) 경로 탐색 실패: 종단경사 한계(26%)·최소곡선반지름(12m)·
|
||||
회피지역 제약으로 통과 경로가 없습니다.
|
||||
|
||||
그 결과 배수유역·관은 새 노선으로 가고 종횡단만 옛 노선에 남아 배수관 측점이 9 → 0 이 됐다.
|
||||
PLAN 0-3 에서 **노선 자동탐색은 접기로** 했고(사용자 확정), 계획노선은 사용자가 직접 고친다.
|
||||
여기서는 그 노선을 **그대로 받아** 표고만 지표면에서 떠서 결과 꼴을 맞춘다.
|
||||
|
||||
**위반은 세되 막지 않는다** — 종단기울기·곡선반지름을 재어 개수로 알리되 경로를 바꾸지
|
||||
않는다(사용자 확정: 자동 보정·차단 없이 경고만). 막는 순간 「내가 그린 선이 안 들어간다」가 된다.
|
||||
|
||||
솔버(`B05_Profile_Engine_Solver`)와 지표 계산이 겹치지만 **합치지 않았다** — 솔버는 0-3 으로
|
||||
접히는 코드라 그쪽을 리팩터링해 두 곳을 얽을 값어치가 없다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_MODELS_SUBDIR = "B04_PreProcess/models"
|
||||
|
||||
|
||||
def _elevation_lookup(
|
||||
project_root: Path, filter_key: str, method: str, smooth: bool
|
||||
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""지표면 격자(x, y, z) — 노선 정점의 표고를 뜨는 데 쓴다."""
|
||||
from B05_Profile.B05_Profile_Engine_Solver import _load_dtm_grid, _sample_surface_on_grid
|
||||
|
||||
models_dir = Path(project_root) / _MODELS_SUBDIR
|
||||
x_coords, y_coords, dtm_z, _valid = _load_dtm_grid(models_dir, filter_key, smooth)
|
||||
surface_z = _sample_surface_on_grid(
|
||||
models_dir, filter_key, method, smooth, x_coords, y_coords, dtm_z
|
||||
)
|
||||
return x_coords, y_coords, surface_z
|
||||
|
||||
|
||||
def _z_at(
|
||||
x_coords: np.ndarray, y_coords: np.ndarray, z_grid: np.ndarray, x: float, y: float
|
||||
) -> float:
|
||||
"""격자에서 가장 가까운 칸의 표고. 격자 밖이면 가장자리 값."""
|
||||
col = int(np.clip(np.searchsorted(x_coords, x), 0, len(x_coords) - 1))
|
||||
row = int(np.clip(np.searchsorted(y_coords, y), 0, len(y_coords) - 1))
|
||||
value = float(z_grid[row, col])
|
||||
return value if math.isfinite(value) else 0.0
|
||||
|
||||
|
||||
def solve_as_planned(
|
||||
project_root: Path,
|
||||
filter_key: str,
|
||||
smooth: bool,
|
||||
vertices: list[tuple[float, float]],
|
||||
options: dict[str, Any],
|
||||
method: str = "dtm",
|
||||
) -> dict[str, Any]:
|
||||
"""계획노선 정점을 그대로 노선으로 삼는다. 반환 꼴은 솔버와 같다."""
|
||||
if len(vertices) < 2:
|
||||
raise ValueError("계획노선 정점이 2개 미만입니다.")
|
||||
x_coords, y_coords, z_grid = _elevation_lookup(project_root, filter_key, method, smooth)
|
||||
polyline = [[float(x), float(y), _z_at(x_coords, y_coords, z_grid, x, y)] for x, y in vertices]
|
||||
|
||||
max_uphill = float(options.get("max_uphill_grade") or 0.26)
|
||||
max_downhill = float(options.get("max_downhill_grade") or 0.26)
|
||||
min_radius = float(options.get("min_curve_radius_m") or 12.0)
|
||||
|
||||
count = len(polyline)
|
||||
chainage_m = [0.0] * count
|
||||
length_m = 0.0
|
||||
grade_sums = 0.0
|
||||
max_grade = 0.0
|
||||
max_up = 0.0
|
||||
max_down = 0.0
|
||||
slope_violations = 0
|
||||
for index in range(count - 1):
|
||||
x1, y1, z1 = polyline[index]
|
||||
x2, y2, z2 = polyline[index + 1]
|
||||
horizontal = math.hypot(x2 - x1, y2 - y1)
|
||||
chainage_m[index + 1] = chainage_m[index] + horizontal
|
||||
if horizontal <= 0.01:
|
||||
continue
|
||||
dz = z2 - z1
|
||||
slope = abs(dz) / horizontal
|
||||
length_m += horizontal
|
||||
grade_sums += slope * horizontal
|
||||
max_grade = max(max_grade, slope)
|
||||
if dz > 0:
|
||||
max_up = max(max_up, slope)
|
||||
else:
|
||||
max_down = max(max_down, slope)
|
||||
if slope > (max_uphill if dz > 0 else max_downhill):
|
||||
slope_violations += 1
|
||||
avg_grade = (grade_sums / length_m) if length_m > 0 else 0.0
|
||||
|
||||
# 곡선반지름 — 세 점을 지나는 원으로 재고, 하한을 밑도는 자리를 **세기만** 한다.
|
||||
from B05_Profile.B05_Profile_Engine_Geometry import circumradius_2d
|
||||
|
||||
curve_violations = 0
|
||||
min_radius_actual = float("inf")
|
||||
for index in range(1, count - 1):
|
||||
radius = circumradius_2d(polyline[index - 1], polyline[index], polyline[index + 1])
|
||||
min_radius_actual = min(min_radius_actual, radius)
|
||||
if radius < min_radius:
|
||||
curve_violations += 1
|
||||
|
||||
total_length = chainage_m[-1] if chainage_m else 0.0
|
||||
segments = [
|
||||
{
|
||||
"index": 0,
|
||||
"from": "BP",
|
||||
"to": "EP",
|
||||
"point_start": 0,
|
||||
"point_end": count - 1,
|
||||
"chainage_start_m": 0.0,
|
||||
"chainage_end_m": round(total_length, 2),
|
||||
"length_m": round(total_length, 2),
|
||||
"max_grade_pct": round(max_grade * 100, 2),
|
||||
}
|
||||
]
|
||||
logger.info(
|
||||
"계획노선 그대로 사용: 정점 %d · 연장 %.1fm · 경사위반 %d · 곡선위반 %d",
|
||||
count,
|
||||
total_length,
|
||||
slope_violations,
|
||||
curve_violations,
|
||||
)
|
||||
return {
|
||||
"polyline": polyline,
|
||||
"chainage_m": [round(value, 3) for value in chainage_m],
|
||||
"segments": segments,
|
||||
# 사용자가 그린 선이므로 제어점 도달 검사는 뜻이 없다 — 통과로 둔다.
|
||||
"required_point_checks": [],
|
||||
"required_points_ok": True,
|
||||
"avoid_intrusions": [],
|
||||
"forbidden_intrusions": [],
|
||||
"curve_warning_segments": [],
|
||||
"avoid_retry_performed": False,
|
||||
"conditions_snapshot": {
|
||||
"filter": filter_key,
|
||||
"method": method,
|
||||
"smooth": smooth,
|
||||
"source": "as_planned",
|
||||
"min_curve_radius_m": round(min_radius, 2),
|
||||
},
|
||||
"metrics": {
|
||||
"length_m": round(length_m, 2),
|
||||
"avg_grade_pct": round(avg_grade * 100, 2),
|
||||
"max_grade_pct": round(max_grade * 100, 2),
|
||||
"max_uphill_pct": round(max_up * 100, 2),
|
||||
"max_downhill_pct": round(max_down * 100, 2),
|
||||
"slope_violations": slope_violations,
|
||||
"search_max_grade_pct": round(max(max_uphill, max_downhill) * 100, 2),
|
||||
"curve_violations": curve_violations,
|
||||
"min_curve_radius_m": round(min_radius_actual, 2)
|
||||
if math.isfinite(min_radius_actual)
|
||||
else None,
|
||||
"min_curve_radius_limit_m": round(min_radius, 2),
|
||||
},
|
||||
}
|
||||
@@ -67,7 +67,10 @@ class RouteSolveRequest(BaseModel):
|
||||
method: str = Field(default="dtm", description="지표면 표현 (dtm/tin/nurbs/implicit/meshfree)")
|
||||
smooth: bool = Field(default=False)
|
||||
surface_model_id: int | None = Field(default=None, description="기반 지표면 모델 id")
|
||||
algorithm: str = Field(default="dijkstra", description="경로 알고리즘 (dijkstra/ridge_valley)")
|
||||
algorithm: str = Field(
|
||||
default="dijkstra",
|
||||
description="경로 알고리즘 (dijkstra/ridge_valley/as_planned)",
|
||||
)
|
||||
|
||||
bp: RoutePoint
|
||||
ep: RoutePoint
|
||||
@@ -116,7 +119,8 @@ class RouteSolveRequest(BaseModel):
|
||||
def validate_choices(self) -> "RouteSolveRequest":
|
||||
if self.grade_class not in ROUTE_GRADE_CLASSES:
|
||||
raise ValueError(f"임도 등급은 {ROUTE_GRADE_CLASSES} 중 하나여야 합니다.")
|
||||
if self.algorithm not in ("dijkstra", "ridge_valley"):
|
||||
# `as_planned` = 사용자가 고친 계획노선을 그대로 씀(PLAN 0-3 자동탐색 접기).
|
||||
if self.algorithm not in ("dijkstra", "ridge_valley", "as_planned"):
|
||||
raise ValueError("경로 알고리즘은 dijkstra 또는 ridge_valley여야 합니다.")
|
||||
if self.design_speed_kph is not None and self.design_speed_kph not in (20, 30, 40):
|
||||
raise ValueError("설계속도는 20/30/40 중 하나여야 합니다.")
|
||||
|
||||
Reference in New Issue
Block a user