diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine_SheetView.py b/B04_wf1_Surface/B04_wf1_Surface_Engine_SheetView.py deleted file mode 100644 index 2db00f54..00000000 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine_SheetView.py +++ /dev/null @@ -1,165 +0,0 @@ -# B04_wf1_Surface_Engine_SheetView.py -# 도엽 병합본의 화면 표시용 사본 생성. -# -# 배경(2026-08-01 실측): 9매 병합 등고선(도엽_등고선.geojson)은 37.3MB다. 도엽 zip 원본은 -# 9매 합쳐 8.9MB지만, 등고선 레이어만 뽑아 비압축 텍스트로 펼치면서 커진다 -# (정점 815,471개 × 좌표 소수 14자리). 화면은 프로젝트 주변만 보므로 그만큼 보낼 이유가 없다. -# -# 분석용 원본은 건드리지 않는다 — 배수유역 계산은 상류 유역까지 필요해 넓은 범위를 쓴다. -# 여기서는 프로젝트 주변만 잘라 좌표 자릿수를 줄인 별도 사본을 만들어 전송에만 쓴다. - -from __future__ import annotations - -import json -import logging -import math -from pathlib import Path -from typing import Any - -from config.config_system import ( - SHEET_VIEW_COORD_DECIMALS, - SHEET_VIEW_MARGIN_M, - SHEET_VIEW_SUFFIX, -) - -logger = logging.getLogger(__name__) - -_METERS_PER_LAT_DEGREE = 111_320.0 - - -def view_path_for(source_path: Path) -> Path: - """원본 geojson 경로에 대응하는 표시용 사본 경로.""" - return source_path.with_name(f"{source_path.stem}{SHEET_VIEW_SUFFIX}{source_path.suffix}") - - -def _crop_box(processed_dir: Path) -> tuple[float, float, float, float] | None: - """표시 범위(경도/위도 최소·최대). VWorld 메타의 프로젝트 범위 + 여유폭.""" - meta_path = processed_dir / "vworld_white_meta.json" - if not meta_path.is_file(): - meta_path = processed_dir / "vworld_satellite_meta.json" - if not meta_path.is_file(): - return None - try: - meta = json.loads(meta_path.read_text(encoding="utf-8")) - lon_min = float(meta["lon_min"]) - lon_max = float(meta["lon_max"]) - lat_min = float(meta["lat_min"]) - lat_max = float(meta["lat_max"]) - except (OSError, ValueError, KeyError) as exc: - logger.warning("도엽 표시본: 메타 범위 읽기 실패 (%s)", exc) - return None - - lat_margin = SHEET_VIEW_MARGIN_M / _METERS_PER_LAT_DEGREE - center_lat_rad = math.radians((lat_min + lat_max) / 2.0) - lon_scale = max(math.cos(center_lat_rad), 0.1) - lon_margin = SHEET_VIEW_MARGIN_M / (_METERS_PER_LAT_DEGREE * lon_scale) - return ( - lon_min - lon_margin, - lat_min - lat_margin, - lon_max + lon_margin, - lat_max + lat_margin, - ) - - -def _round_coordinates(value: Any) -> Any: - """좌표 배열을 재귀적으로 훑어 소수 자릿수를 줄인다(고도값 포함). - - shapely는 좌표를 튜플로 돌려주므로 list/tuple 둘 다 받는다. - """ - if isinstance(value, (list, tuple)): - if value and isinstance(value[0], (int, float)): - return [round(float(number), SHEET_VIEW_COORD_DECIMALS) for number in value] - return [_round_coordinates(item) for item in value] - return value - - -def _feature_bbox(coordinates: Any) -> tuple[float, float, float, float] | None: - """지오메트리 좌표 전체를 훑어 bbox를 구한다.""" - lons: list[float] = [] - lats: list[float] = [] - - def walk(node: Any) -> None: - if isinstance(node, list): - if node and isinstance(node[0], (int, float)): - lons.append(float(node[0])) - lats.append(float(node[1])) - return - for item in node: - walk(item) - - walk(coordinates) - if not lons: - return None - return (min(lons), min(lats), max(lons), max(lats)) - - -def build_sheet_view(source_path: Path, processed_dir: Path) -> Path | None: - """도엽 병합본에서 표시용 사본을 만들어 경로를 반환한다. - - - 표시 범위(프로젝트 + 여유폭)와 겹치는 지물만 남긴다(경계에서 자르지 않고 통째로 유지). - - 좌표 소수 자릿수를 줄이고 공백 없는 JSON으로 기록한다. - - 이미 최신 사본이 있으면 그대로 돌려준다(원본보다 오래되면 다시 만든다). - - 표시 범위를 알 수 없으면 None — 호출측이 원본을 그대로 보내면 된다. - """ - view_path = view_path_for(source_path) - if view_path.is_file() and view_path.stat().st_mtime >= source_path.stat().st_mtime: - return view_path - - box = _crop_box(processed_dir) - if box is None: - return None - min_lon, min_lat, max_lon, max_lat = box - - try: - data = json.loads(source_path.read_text(encoding="utf-8")) - except (OSError, ValueError) as exc: - logger.warning("도엽 표시본: 원본 읽기 실패 %s (%s)", source_path.name, exc) - return None - - from shapely.geometry import box, mapping, shape - - crop_area = box(min_lon, min_lat, max_lon, max_lat) - kept: list[dict[str, Any]] = [] - for feature in data.get("features") or []: - geometry = feature.get("geometry") or {} - coordinates = geometry.get("coordinates") - if coordinates is None: - continue - bbox = _feature_bbox(coordinates) - if bbox is None: - continue - if bbox[0] > max_lon or bbox[2] < min_lon or bbox[1] > max_lat or bbox[3] < min_lat: - continue - # 등고선 한 가닥은 도엽 끝까지 이어진다. 겹친다고 통째로 남기면 화면 밖 구간까지 - # 보내게 되므로 표시 범위에서 잘라낸다(고도 등 속성은 그대로 유지). - try: - clipped = shape(geometry).intersection(crop_area) - except Exception: - continue - if clipped.is_empty: - continue - clipped_geometry = mapping(clipped) - feature["geometry"] = { - "type": clipped_geometry["type"], - "coordinates": _round_coordinates(clipped_geometry["coordinates"]), - } - kept.append(feature) - - payload = {"type": "FeatureCollection", "features": kept} - if data.get("crs"): - payload["crs"] = data["crs"] - - tmp_path = view_path.with_suffix(".tmp") - tmp_path.write_text( - json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8" - ) - tmp_path.replace(view_path) - logger.info( - "도엽 표시본 생성: %s %d건 → %s (%.1fMB → %.1fMB)", - source_path.name, - len(kept), - view_path.name, - source_path.stat().st_size / 1e6, - view_path.stat().st_size / 1e6, - ) - return view_path diff --git a/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py b/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py index 60baa091..58b70861 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Router_GIS.py @@ -9,7 +9,6 @@ from fastapi import APIRouter, HTTPException, Request, Response from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path -from B04_wf1_Surface.B04_wf1_Surface_Engine_SheetView import build_sheet_view from common_util.common_util_http_cache import cached_file_response from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool @@ -149,12 +148,9 @@ async def get_project_geojson( ) if layer in _SHEET_GEOJSON_FILES: - view_path = await asyncio.to_thread(build_sheet_view, filepath, target_dir) - return cached_file_response( - request, - view_path or filepath, - "application/geo+json", - ) + # 도엽 산출물은 자르거나 줄이지 않고 파일 그대로 보낸다(2026-08-01 사용자 지시). + # 재직렬화만 건너뛰어도 요청당 2초가 사라지고, ETag로 두 번째부터는 304가 된다. + return cached_file_response(request, filepath, "application/geo+json") if layer == "등고선": simplified_filepath = target_dir / "등고선_bounds_simplified.geojson" diff --git a/config/config_system.py b/config/config_system.py index d94fc375..c3839ef2 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -516,14 +516,6 @@ STORAGE_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "sto MAP_SHEETS_DIRNAME = "map_sheets" MAP_SHEETS_INDEX_FILENAME = "map_sheets_index.json" -# 도엽 병합본(도엽_*.geojson)의 화면 표시용 사본 — 분석용 원본은 그대로 두고, -# 브라우저로 보낼 때만 프로젝트 주변으로 잘라 좌표 자릿수를 줄인 사본을 쓴다. -# (9매 병합 등고선 원본 37MB → 표시용 1MB 안팎, 2026-08-01) -SHEET_VIEW_SUFFIX = "_view" -SHEET_VIEW_MARGIN_M = 500.0 -# 소수 7자리 ≈ 1cm — 1:5,000 도엽 표시 정밀도에 충분하다. -SHEET_VIEW_COORD_DECIMALS = 7 - # 배경 지도(위성·하이브리드·백지도) 내려받을 범위의 여유폭. # 라이다 범위와 계획노선을 합친 뒤 이만큼 넓혀서 받는다 — 계획노선이 라이다 범위를 벗어나도 # 배경이 잘리지 않게 한다. 브이월드 위성사진은 더 높은 해상도를 주지 않으므로 도엽만큼