- POST /plan/lidar(LAS · LAZ + .prj · 예정노선만 · 다시 올리면 갈음) → 뒤에서 지형(진행 = /plan/progress lidar) · GET /plan/lidar - 자리 = D 의 B04_PreProcess/lidar/(안은 B 와 같은 꼴) · LAS 좌표계 다르면 x · y 옮겨 적음 · DTM = surface_models 행(높이 격자) - terrain=lidar 급경사 · 음영 · 0차곡 = 올린 DTM · 범위 밖 = 빈칸(수치지형도로 안 메움 · 결정 A⑥) - B04 run_surface_analysis 에 maps 인자 덧(기본 True = B 그대로 · False = 지도 · GIS 받기 · 도엽 서피스 건너뜀) - 시험 test_624_4_plan_lidar 4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
8.0 KiB
Python
181 lines
8.0 KiB
Python
"""D03 지도 겹의 프로젝트 틀 · 지형 원천 (요구 530 · 531 · PLAN 624-1).
|
|
|
|
겹 길(`/plan/layers/*`)은 프로젝트 종류를 안 가림:
|
|
|
|
- 예정노선(PLAN) = 받은 범위 · 지형 = 수치지형도(`models_plan/dtm_sheet_<방법>.npz`) ·
|
|
`lidar` = D 에 올린 LAS 로 지은 DTM(624-4 · `D03_PlanAnalysis_Lidar` · 안 올렸으면 404 ·
|
|
범위 밖 = 빈칸)
|
|
- 그 밖(임도 등) = 범위 = B04 배경 지도 상자(`vworld_white_meta.json` · 없으면 원천 격자 상자) ·
|
|
지형 = 설계 지형(stage 1 params · 기본) · `lidar` = 라이다 확정 지표면 ·
|
|
`sheet` = 수치지형도 서피스 ·
|
|
평활 고르기 = 서버 표본기와 같음(`B05_Profile_Router_Terrain.dtm_grid_npz`)
|
|
- 원천 인자 `terrain=sheet|lidar|design`(빈 값 = design) · 파생 저장본 열쇠 = 격자 파일 이름(원천)
|
|
- 받은 겹 저장 = 종류 상관없이 `D03_PlanAnalysis/layers/`(B04 `processed/` 파일과 이름 겹침 0) ·
|
|
D 옛 자리(`B04_PreProcess/processed/`) 파일은 새 자리에 없을 때 한 번 복사(다시 받지 않게)
|
|
|
|
계산 자리 = ③ 서버 단독.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any, Callable, NamedTuple
|
|
|
|
import numpy as np
|
|
from fastapi import HTTPException
|
|
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from config.config_system_terrain import SHEET_SURFACE_DEFAULT_METHOD
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Landslide as landslide
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Lidar as lidar
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Store as store
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_Terrain as terrain
|
|
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero_order
|
|
|
|
Box = tuple[float, float, float, float]
|
|
|
|
SOURCES = ("sheet", "lidar", "design")
|
|
LAYER_DIR = Path("D03_PlanAnalysis") / "layers"
|
|
OLD_DIR = Path("B04_PreProcess") / "processed"
|
|
ROAD_MODELS = Path("B04_PreProcess") / "models"
|
|
ROAD_META = ("vworld_white_meta.json", "vworld_satellite_meta.json")
|
|
|
|
|
|
def _legacy_names() -> list[str]:
|
|
names = [layers.CADASTRAL_FILE, layers.CADASTRAL_KEY, layers.ZONE_FILE, layers.ZONE_KEY]
|
|
names += [layers.SOIL_FILE, layers.SOIL_KEY, landslide.LANDSLIDE_FILE]
|
|
for label, _ in layers.LINE_KINDS.values():
|
|
names += [f"{label}_bounds.geojson", f"{label}_bounds.key.json"]
|
|
return names
|
|
|
|
|
|
class Frame(NamedTuple):
|
|
root: Path
|
|
epsg: int
|
|
box: Box
|
|
store: Path # 받은 겹 자리(D03 몫)
|
|
source: str # 실제 원천 — sheet · lidar
|
|
dem: Path | None # 원천 격자(없으면 None)
|
|
|
|
|
|
def check_source(choice: str | None) -> str:
|
|
if choice in (None, ""):
|
|
return "design"
|
|
if choice not in SOURCES:
|
|
raise HTTPException(status_code=400, detail=f"없는 지형 원천: {choice}")
|
|
return choice
|
|
|
|
|
|
def layer_store(root: Path, plan: bool) -> Path:
|
|
"""받은 겹 자리 — D 는 옛 자리 파일을 처음 한 번 복사(덮지 않음)."""
|
|
store = Path(root) / LAYER_DIR
|
|
store.mkdir(parents=True, exist_ok=True)
|
|
if plan:
|
|
for name in _legacy_names():
|
|
old, new = Path(root) / OLD_DIR / name, store / name
|
|
if old.is_file() and not new.exists():
|
|
shutil.copy2(old, new)
|
|
return store
|
|
|
|
|
|
def plan_frame(root: Path, epsg: int, box: Box, choice: str | None) -> Frame:
|
|
"""예정노선 — 수치지형도 하나(design = sheet) · lidar = 올린 LAS 의 DTM(없으면 None)."""
|
|
source = check_source(choice)
|
|
source = "sheet" if source == "design" else source
|
|
npz = terrain.models_dir_of(Path(root)) / f"dtm_sheet_{SHEET_SURFACE_DEFAULT_METHOD}.npz"
|
|
dem = npz if source == "sheet" else lidar.lidar_dem(root)
|
|
return Frame(Path(root), epsg, box, layer_store(root, True), source, dem)
|
|
|
|
|
|
def road_dem(models: Path, params: dict[str, Any], source: str) -> tuple[str, Path | None]:
|
|
"""(실제 원천, 격자) — 고른 원천 params 의 `dtm_<필터>`(평활이면 `_smooth` 먼저) ·
|
|
라이다 없는 프로젝트의 lidar = None."""
|
|
from common_util.common_util_surface_confirmation import (
|
|
analysis_terrain_params,
|
|
design_terrain,
|
|
)
|
|
|
|
want = None if source == "design" else ("las" if source == "lidar" else "sheet")
|
|
picked = analysis_terrain_params(params, want)
|
|
got = "sheet" if design_terrain(picked) == "sheet" else "lidar"
|
|
if source != "design" and got != source:
|
|
return source, None
|
|
npz = Path(models) / f"dtm_{picked.get('source_filter') or ''}.npz"
|
|
smooth = npz.with_name(f"{npz.stem}_smooth.npz")
|
|
npz = smooth if picked.get("smooth") and smooth.is_file() else npz
|
|
return got, npz if npz.is_file() else None
|
|
|
|
|
|
def grid_box(npz: Path) -> Box:
|
|
with np.load(npz) as dem:
|
|
x, y = np.asarray(dem["x"], float), np.asarray(dem["y"], float)
|
|
half = abs(float(x[1] - x[0])) / 2 if len(x) > 1 else 0.5
|
|
return (x[0] - half, y[0] - half, x[-1] + half, y[-1] + half)
|
|
|
|
|
|
def road_box(root: Path, dem: Path | None) -> Box | None:
|
|
"""B04 배경 지도 상자(작업 좌표계 m) · 없으면 격자 상자."""
|
|
for name in ROAD_META:
|
|
try:
|
|
meta = json.loads((Path(root) / OLD_DIR / name).read_text(encoding="utf-8"))
|
|
return (meta["x_min"], meta["y_min"], meta["x_max"], meta["y_max"])
|
|
except (OSError, ValueError, KeyError, TypeError):
|
|
continue
|
|
return grid_box(dem) if dem is not None else None
|
|
|
|
|
|
async def road_frame(row: dict[str, Any], project_id: Any, choice: str | None) -> Frame:
|
|
"""임도 등 — 범위 · 좌표계 · 설계 지형 params 로 원천 격자."""
|
|
from pyproj import CRS
|
|
|
|
from common_util.common_util_crs import identify_epsg, resolve_project_crs
|
|
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
|
from config.config_db import get_db_pool
|
|
|
|
source = check_source(choice)
|
|
root = Path(resolve_stored_project_path(row["storage_path"]))
|
|
async with get_db_pool().acquire() as connection:
|
|
params = await get_surface_confirmation_params(connection, str(project_id))
|
|
got, dem = road_dem(root / ROAD_MODELS, params, source)
|
|
box = road_box(root, dem)
|
|
if box is None:
|
|
raise HTTPException(status_code=404, detail="지형 전처리 뒤 보입니다.")
|
|
text = resolve_project_crs(root)
|
|
epsg = identify_epsg(CRS.from_user_input(text), text)
|
|
if epsg is None:
|
|
raise HTTPException(status_code=404, detail="프로젝트 좌표계를 모릅니다.")
|
|
return Frame(root, epsg, tuple(float(v) for v in box), layer_store(root, False), got, dem)
|
|
|
|
|
|
def need_dem(frame: Frame) -> Path:
|
|
if frame.dem is None:
|
|
what = "라이다" if frame.source == "lidar" else "수치지형도"
|
|
raise HTTPException(status_code=404, detail=f"{what} 지형이 없습니다.")
|
|
return frame.dem
|
|
|
|
|
|
async def layer_frame(project_id: Any, source: str | None, plan_ends: Callable) -> Frame:
|
|
"""겹 틀 — 예정노선 = 받은 범위(받기 전 404) · 그 밖 = `road_frame`."""
|
|
row = await store.project_row(project_id)
|
|
if row["project_kind"] != "PLAN":
|
|
return await road_frame(row, project_id, source)
|
|
root, epsg, _, _ = await plan_ends(project_id)
|
|
box = terrain.received_box(root, epsg)
|
|
if box is None:
|
|
raise HTTPException(status_code=404, detail="지형 받기 뒤 보입니다.")
|
|
return plan_frame(root, epsg, box, source)
|
|
|
|
|
|
def zero_order_calls(frame: Frame, min_m2: float, max_m2: float) -> tuple[Callable, tuple, tuple]:
|
|
"""(셈, 가벼운 인자 = 저장본만, 무거운 인자) — 수치지형도 = 창고 도엽 선(범위 = 원천 격자
|
|
상자) · 라이다 = 격자 통째."""
|
|
npz = need_dem(frame)
|
|
if frame.source == "sheet":
|
|
args = (frame.root, npz.parent, frame.epsg, min_m2, max_m2)
|
|
return zero_order.zero_order_layer, (*args, False, npz), (*args, True, npz)
|
|
args = (frame.root, npz, frame.epsg, min_m2, max_m2)
|
|
return zero_order.dem_zero_order_layer, (*args, False), args
|