Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_Terrain.py
T
eomsangdonandClaude Opus 5.5 83d38681df fix(530-2): D 분석 지점 0.1 mm 반올림 — 노선 끝점 = 지점 · 연도 경계 정확히 맞물림 · 복도 300 m 실측 근거 주석
- 엔진 출력 자리수(4)로 지점을 맞춰 넣음 · 저장본 지점 대조 1 mm
- 실측(검증_sub3 근처 두 구간): 복도 100 · 200 · 300 m 못 지킨 곳 5 · 8 · 1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:47:30 +09:00

222 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""D 예정노선 지형 받기 — 지점 기준 수치지형도 · 위성 · 연도 구간별 DEM (요구 345 · PLAN 530-1).
지형은 D 프로젝트 storage 안 `B04_PreProcess/` 배치 그대로(지형 코드 주인 = B04 · 고칠 곳 0):
- 도엽 = 모든 지점 + 구간 표본(`SAMPLE_STEP_M`) 각 3×3 → `ensure_sheets`(없는 것만 · prune 없음)
- 병합 = 지난번 병합 도엽 ∪ 이번 도엽(옛 도엽 유지 · 열쇠 같으면 건너뜀)
- 위성 = 기준 도엽 도곽 ∪ 옛 위성 범위(덮으면 건너뜀)
- DEM = 연도 구간마다 `models_<연도>/`(긴 노선 메모리 · 비용면 한도) · 범위 = 구간 상자 + 탐색 복도
- 작업 좌표계 = 지점 epsg · `B03_FileInput/input/prj/plan.prj` 한 장(B04 · B05 좌표계 창구가 읽음)
계산 자리 = ③ 서버 단독. DB 행(`surface_models`)은 라우터가 씀.
"""
from __future__ import annotations
import json
import logging
import math
from pathlib import Path
from typing import Any, Callable
import numpy as np
from B04_PreProcess.B04_PreProcess_Engine_MapSheet import (
neighbors_for_points,
sheet5k_to_bounds,
sheets_for_points,
)
from B04_PreProcess.B04_PreProcess_Engine_SheetParser import parse_and_merge_sheets
from B04_PreProcess.B04_PreProcess_Engine_SheetStore import (
ensure_sheets,
get_project_map_sheets_dir,
get_sheet_path,
)
from B04_PreProcess.B04_PreProcess_Engine_SheetSurface import build_sheet_surface_model
from B04_PreProcess.B04_PreProcess_Engine_VWorld import download_vworld_satellite_map
from common_util.common_util_json import atomic_write_json
from config.config_system_terrain import SHEET_SURFACE_DEFAULT_METHOD, SHEET_SURFACE_MARGIN_M
logger = logging.getLogger(__name__)
# 구간 표본 간격 — 도엽(≈2.8 × 2.3 km) 3×3 묶음이 끊기지 않게
SAMPLE_STEP_M = 1000.0
# 탐색 복도 반폭(D 만 · B 기본 100 m) — 실측(530-2): 100 · 200 m 는 못 지킨 곳 남음 · 300 m 0~1
PLAN_CORRIDOR_M = 300.0
# DEM 이 복도 밖으로 더 덮는 폭(복도 끝 칸 기울기 · 횡단 표본 ±20 m)
DEM_EXTRA_M = 50.0
STAGE = Path("B04_PreProcess")
PRJ_NAME = "plan.prj"
Progress = Callable[[int, str], None]
def models_dir_of(project_root: Path, year: int) -> Path:
"""연도 구간 DEM 자리."""
return project_root / STAGE / f"models_{int(year)}"
def segment_samples(
start: tuple[float, float], end: tuple[float, float]
) -> list[tuple[float, float]]:
"""구간 위 표본(끝점 둘 포함 · `SAMPLE_STEP_M` 간격 이하)."""
length = math.hypot(end[0] - start[0], end[1] - start[1])
count = max(1, math.ceil(length / SAMPLE_STEP_M))
return [
(start[0] + (end[0] - start[0]) * i / count, start[1] + (end[1] - start[1]) * i / count)
for i in range(count + 1)
]
def reference_points(segments: list[dict[str, Any]], epsg: int) -> list[tuple[float, float]]:
"""도엽 기준 좌표(위도, 경도) — 모든 지점 + 구간 표본."""
from pyproj import Transformer
to_wgs = Transformer.from_crs(f"EPSG:{epsg}", "EPSG:4326", always_xy=True)
points: list[tuple[float, float]] = []
for seg in segments:
for x, y in segment_samples(seg["start"], seg["end"]):
lon, lat = to_wgs.transform(x, y)
points.append((lat, lon))
return points
def write_plan_prj(project_root: Path, epsg: int) -> Path:
"""작업 좌표계 .prj — 내용이 같으면 그대로."""
from pyproj import CRS
path = project_root / "B03_FileInput" / "input" / "prj" / PRJ_NAME
text = CRS.from_epsg(int(epsg)).to_wkt("WKT1_ESRI")
if not path.is_file() or path.read_text(encoding="utf-8", errors="ignore") != text:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def dem_outline(start: tuple[float, float], end: tuple[float, float]) -> np.ndarray:
"""DEM 상자 꼭짓점 둘 — B 여유(`SHEET_SURFACE_MARGIN_M`)를 더하면 복도 + `DEM_EXTRA_M`."""
pad = max(0.0, PLAN_CORRIDOR_M + DEM_EXTRA_M - SHEET_SURFACE_MARGIN_M)
xs, ys = (start[0], end[0]), (start[1], end[1])
return np.array(
[[min(xs) - pad, min(ys) - pad], [max(xs) + pad, max(ys) + pad]], dtype=np.float64
)
def _read_json(path: Path) -> dict[str, Any] | None:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
def _merge_sheets(processed: Path, store: Path, available: list[str]) -> list[str]:
"""병합 = 지난번 병합 도엽(zip 남은 것) ∪ 이번 도엽 · 열쇠 같으면 건너뜀."""
key_path = processed / "sheet_merge.key.json"
old = (_read_json(key_path) or {}).get("sheets") or []
sheets = sorted({*available, *(s for s in old if get_sheet_path(store, s) is not None)})
key = {"sheets": sheets}
if sheets and (_read_json(key_path) != key or not any(processed.glob("도엽_*.geojson"))):
parse_and_merge_sheets(store, sheets, processed)
atomic_write_json(key_path, key)
return sheets
def _satellite(processed: Path, prj: Path, points: list[tuple[float, float]], epsg: int) -> str:
"""위성 = 기준 도엽 도곽 ∪ 옛 범위 · 이미 덮으면 건너뜀. 돌려줌 = skip · done · failed."""
from pyproj import Transformer
to_metric = Transformer.from_crs("EPSG:4326", f"EPSG:{epsg}", always_xy=True)
xs: list[float] = []
ys: list[float] = []
for sheet in sheets_for_points(points):
lon0, lat0, lon1, lat1 = sheet5k_to_bounds(sheet)
for lon, lat in ((lon0, lat0), (lon0, lat1), (lon1, lat0), (lon1, lat1)):
x, y = to_metric.transform(lon, lat)
xs.append(x)
ys.append(y)
need = {"x": [min(xs), max(xs)], "y": [min(ys), max(ys)]}
meta = _read_json(processed / "vworld_satellite_meta.json")
try:
old = [float(meta[k]) for k in ("x_min", "x_max", "y_min", "y_max")] if meta else None
except (KeyError, TypeError, ValueError):
old = None
if old and old[0] <= need["x"][0] and old[1] >= need["x"][1]:
if old[2] <= need["y"][0] and old[3] >= need["y"][1]:
return "skip"
if old:
need = {
"x": [min(old[0], need["x"][0]), max(old[1], need["x"][1])],
"y": [min(old[2], need["y"][0]), max(old[3], need["y"][1])],
}
try:
done = download_vworld_satellite_map(prj, need, processed, layer_name="Satellite")
except Exception as exc: # noqa: BLE001 — 위성은 배경 · 지형 받기를 막지 않음
logger.warning("D 위성 지도 받기 실패: %s", exc)
return "failed"
return "done" if done else "failed"
def ensure_plan_terrain(
project_root: Path,
segments: list[dict[str, Any]],
epsg: int,
on_progress: Progress | None = None,
) -> dict[str, Any]:
"""지점 기준 지형 받기 — `segments` = [{year, start: (x, y), end: (x, y)}](작업 좌표계 m).
돌려줌 = 도엽(받은 · 실패 · 병합) · 위성 상태 · 연도별 모델 등록용 dict 목록.
"""
def report(percent: int, message: str) -> None:
if on_progress is not None:
on_progress(percent, message)
if not segments:
raise ValueError("연도 구간이 없습니다.")
processed = project_root / STAGE / "processed"
processed.mkdir(parents=True, exist_ok=True)
prj = write_plan_prj(project_root, epsg)
crs = f"EPSG:{int(epsg)}"
report(5, "수치지형도 도엽 고르는 중")
points = reference_points(segments, epsg)
wanted = neighbors_for_points(points)
store = get_project_map_sheets_dir(project_root)
before = {s for s in wanted if get_sheet_path(store, s) is not None}
report(10, f"수치지형도 도엽 {len(wanted) - len(before)}매 받는 중")
got = ensure_sheets(store, wanted)
if not got["available"]:
raise ValueError(f"수치지형도 도엽을 하나도 받지 못했습니다: {got['failed']}")
report(45, "도엽 레이어 병합 중")
merged = _merge_sheets(processed, store, list(got["available"]))
report(60, "위성 지도 받는 중")
satellite = _satellite(processed, prj, points, epsg)
years: dict[int, list[dict[str, Any]]] = {}
for i, seg in enumerate(segments):
year = int(seg["year"])
report(65 + 30 * i // len(segments), f"{year}년 구간 지형면 만드는 중")
models_dir = models_dir_of(project_root, year)
models_dir.mkdir(parents=True, exist_ok=True)
years[year] = build_sheet_surface_model(
project_root,
processed,
models_dir,
dem_outline(seg["start"], seg["end"]),
crs,
[SHEET_SURFACE_DEFAULT_METHOD],
)
if not years[year]:
raise ValueError(f"{year}년 구간 지형면을 만들지 못했습니다 — 도엽 등고선 확인")
report(100, "지형 받기 끝")
return {
"sheets": {
"wanted": len(wanted),
"downloaded": sorted(set(got["available"]) - before),
"failed": got["failed"],
"merged": merged,
},
"satellite": satellite,
"years": years,
}