feat(B04): 배경 지도 확보 범위를 기준 도엽 도곽과 동일하게
사용자 지시(2026-08-01): 위성사진은 수치지형도 도엽과 같은 눈금으로 확보한다. 계획노선이 걸치는 기준 도엽만 받고(1매 또는 2~3매), 주변 도엽은 받지 않는다. - sheets_for_points(): 기준 좌표가 속한 도엽번호만 반환(주변 확장 없음) - satellite_extent(): 기준 도엽들의 도곽 합집합을 프로젝트 좌표계로 반환. 도엽을 못 구하면 라이다∪계획노선 범위로 폴백 - 다운로더: 주변 확장 로직 제거. 요청 범위를 덮는 타일만 받되, 한 변 타일 수가 한도를 넘으면 zoom을 낮춘다(도엽 1매를 zoom 18로 받으면 한 변 19타일 = 4,864px) - 국가 GIS 벡터는 종전대로 라이다∪계획노선 범위 사용 - 표본 실측: 기준 도엽 1매(37816093) -> 10x13 타일, 2438x3169m, 0.95m/px, 15.1MB, 실패 타일 0개 (이전 731x853m) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -235,6 +235,7 @@ def run_surface_analysis(
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Engine_Extent import (
|
||||
download_extent,
|
||||
map_meta_covers,
|
||||
satellite_extent,
|
||||
)
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Engine_GisVector import download_all_gis_vectors
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Engine_VWorld import (
|
||||
@@ -250,9 +251,11 @@ def run_surface_analysis(
|
||||
project_epsg = "EPSG:5186"
|
||||
if prj_path.exists():
|
||||
project_epsg = get_epsg_from_prj(prj_path.read_text(encoding="utf-8", errors="ignore"))
|
||||
# 배경 지도는 라이다 범위와 계획노선을 합친 범위로 받는다 — 노선이 라이다 범위를
|
||||
# 벗어나도 배경이 잘리지 않게 한다(2026-08-01 사용자 지시).
|
||||
# 국가 GIS 벡터는 라이다∪계획노선 범위로 받는다.
|
||||
bounds_dict_for_download = download_extent(project_root, las_bounds_dict, project_epsg)
|
||||
# 배경 지도(위성·하이브리드·백지도)는 계획노선이 걸치는 기준 도엽의 도곽 범위로 받는다
|
||||
# — 수치지형도 도엽과 같은 눈금. 주변 도엽은 받지 않는다(2026-08-01 사용자 지시).
|
||||
map_bounds_for_download = satellite_extent(project_root, las_bounds_dict, project_epsg)
|
||||
|
||||
# VWorld 지도 및 GIS 데이터 저장 위치는 B04_wf1_Surface/processed에 보관.
|
||||
layers = [
|
||||
@@ -264,13 +267,13 @@ def run_surface_analysis(
|
||||
meta_path = processed_dir / f"vworld_{item['layer'].lower()}_meta.json"
|
||||
# 파일이 있어도 계획노선·여유 셀이 바뀌어 범위를 못 덮으면 다시 받는다.
|
||||
# (B03 업로드가 부르는 경로는 rebuild=False라, 존재 여부만 보면 영영 갱신되지 않는다.)
|
||||
if not rebuild and map_meta_covers(meta_path, bounds_dict_for_download):
|
||||
if not rebuild and map_meta_covers(meta_path, map_bounds_for_download):
|
||||
continue
|
||||
try:
|
||||
step_started = time.monotonic()
|
||||
download_vworld_satellite_map(
|
||||
prj_path,
|
||||
bounds_dict_for_download,
|
||||
map_bounds_for_download,
|
||||
processed_dir,
|
||||
layer_name=item["layer"],
|
||||
ext=item["ext"],
|
||||
|
||||
@@ -82,6 +82,44 @@ def download_extent(
|
||||
}
|
||||
|
||||
|
||||
def satellite_extent(
|
||||
project_root: Path,
|
||||
las_bounds: dict[str, list[float]],
|
||||
target_epsg: str,
|
||||
) -> dict[str, list[float]]:
|
||||
"""배경 지도를 확보할 범위 = 계획노선이 걸치는 **기준 도엽**의 도곽 범위.
|
||||
|
||||
수치지형도 도엽과 같은 눈금으로 맞춘다 — 도엽 1매면 1매 크기, 2~3매에 걸치면 그만큼.
|
||||
주변 도엽은 받지 않는다(2026-08-01 사용자 지시).
|
||||
도엽 번호를 얻지 못하면 라이다∪계획노선 범위로 되돌아간다.
|
||||
"""
|
||||
from pyproj import Transformer
|
||||
|
||||
from .B04_wf1_Surface_Engine_MapSheet import sheet5k_to_bounds, sheets_for_points
|
||||
|
||||
points = sheet_reference_points_wgs84(project_root, las_bounds, target_epsg)
|
||||
sheets = sheets_for_points(points)
|
||||
if not sheets:
|
||||
return download_extent(project_root, las_bounds, target_epsg)
|
||||
|
||||
lon_min = lat_min = float("inf")
|
||||
lon_max = lat_max = float("-inf")
|
||||
for sheet_no in sheets:
|
||||
s_lon_min, s_lat_min, s_lon_max, s_lat_max = sheet5k_to_bounds(sheet_no)
|
||||
lon_min, lon_max = min(lon_min, s_lon_min), max(lon_max, s_lon_max)
|
||||
lat_min, lat_max = min(lat_min, s_lat_min), max(lat_max, s_lat_max)
|
||||
|
||||
to_target = Transformer.from_crs("EPSG:4326", target_epsg, always_xy=True)
|
||||
x0, y0 = to_target.transform(lon_min, lat_min)
|
||||
x1, y1 = to_target.transform(lon_max, lat_max)
|
||||
logger.info("B04 배경 지도 기준 도엽 %d매: %s", len(sheets), ", ".join(sheets))
|
||||
return {
|
||||
"x": [min(x0, x1), max(x0, x1)],
|
||||
"y": [min(y0, y1), max(y0, y1)],
|
||||
"z": list(las_bounds.get("z", [0.0, 0.0])),
|
||||
}
|
||||
|
||||
|
||||
def map_meta_covers(meta_path: Path, extent: dict[str, list[float]]) -> bool:
|
||||
"""저장된 배경 지도가 필요한 범위를 이미 덮고 있는가.
|
||||
|
||||
|
||||
@@ -90,6 +90,22 @@ def neighbors_3x3(sheet_no: str) -> list[str]:
|
||||
return result
|
||||
|
||||
|
||||
def sheets_for_points(points: list[tuple[float, float]]) -> list[str]:
|
||||
"""기준 좌표들이 속한 도엽번호만(주변 도엽 없음, 중복 제거).
|
||||
|
||||
계획노선 시점·종점이 같은 도엽이면 1매, 걸치면 2~3매가 된다.
|
||||
배경 지도(위성사진)는 이 도엽 범위만 확보한다(2026-08-01 사용자 지시).
|
||||
"""
|
||||
ordered: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for lat, lon in points:
|
||||
sheet_no = latlon_to_sheet5k(lat, lon)
|
||||
if sheet_no not in seen:
|
||||
seen.add(sheet_no)
|
||||
ordered.append(sheet_no)
|
||||
return ordered
|
||||
|
||||
|
||||
def neighbors_for_points(points: list[tuple[float, float]]) -> list[str]:
|
||||
"""기준 좌표들이 속한 도엽 + 각각의 주변 8매를 합친 목록(중복 제거, 순서 유지).
|
||||
|
||||
|
||||
@@ -19,10 +19,14 @@ try:
|
||||
VWORLD_API_KEY = getattr(
|
||||
config_system, "VWORLD_API_KEY", "3DBD7306-7DBD-38BB-B292-267C5ED7AC6B"
|
||||
)
|
||||
SURFACE_MAP_NEIGHBOR_RINGS = getattr(config_system, "SURFACE_MAP_NEIGHBOR_RINGS", 1)
|
||||
SURFACE_MAP_MAX_TILES_PER_SIDE = getattr(config_system, "SURFACE_MAP_MAX_TILES_PER_SIDE", 12)
|
||||
SURFACE_MAP_MAX_ZOOM = getattr(config_system, "SURFACE_MAP_MAX_ZOOM", 18)
|
||||
SURFACE_MAP_MIN_ZOOM = getattr(config_system, "SURFACE_MAP_MIN_ZOOM", 14)
|
||||
except ImportError:
|
||||
VWORLD_API_KEY = "3DBD7306-7DBD-38BB-B292-267C5ED7AC6B"
|
||||
SURFACE_MAP_NEIGHBOR_RINGS = 1
|
||||
SURFACE_MAP_MAX_TILES_PER_SIDE = 12
|
||||
SURFACE_MAP_MAX_ZOOM = 18
|
||||
SURFACE_MAP_MIN_ZOOM = 14
|
||||
|
||||
|
||||
def get_epsg_from_prj(prj_content: str) -> str:
|
||||
@@ -98,38 +102,22 @@ def download_vworld_satellite_map(
|
||||
lon_min, lat_min = transformer.transform(x_min, y_min)
|
||||
lon_max, lat_max = transformer.transform(x_max, y_max)
|
||||
|
||||
# 3. 지도 타일 크기 결정 (ZOOM 18 초고해상도 적용)
|
||||
zoom = 18
|
||||
|
||||
# 영역을 포괄하는 좌상단 타일, 우하단 타일 인덱스 산출
|
||||
x1, y1 = latlon_to_tile(lat_max, lon_min, zoom)
|
||||
x2, y2 = latlon_to_tile(lat_min, lon_max, zoom)
|
||||
|
||||
# 기준 박스 = 계획노선·기준 좌표를 덮는 한 장. 그 주위로 같은 크기의 박스를 더 받는다.
|
||||
# rings=1이면 주변 8장이 붙어 3×3 배치가 된다(2026-08-01 사용자 지시).
|
||||
base_x_start, base_x_end = min(x1, x2), max(x1, x2)
|
||||
base_y_start, base_y_end = min(y1, y2), max(y1, y2)
|
||||
box_w = base_x_end - base_x_start + 1
|
||||
box_h = base_y_end - base_y_start + 1
|
||||
rings = SURFACE_MAP_NEIGHBOR_RINGS
|
||||
x_start = base_x_start - box_w * rings
|
||||
x_end = base_x_end + box_w * rings
|
||||
y_start = base_y_start - box_h * rings
|
||||
y_end = base_y_end + box_h * rings
|
||||
|
||||
# 너무 많은 타일을 내려받아 IP가 막히는 것을 막는다. 한쪽만 자르면 기준 박스가 화면
|
||||
# 가장자리로 밀리므로 양쪽에서 균등하게 줄인다.
|
||||
def _clip(start: int, end: int, limit: int = 15) -> tuple[int, int]:
|
||||
count = end - start + 1
|
||||
if count <= limit:
|
||||
return start, end
|
||||
over = count - limit
|
||||
return start + over // 2, end - (over - over // 2)
|
||||
|
||||
x_start, x_end = _clip(x_start, x_end)
|
||||
y_start, y_end = _clip(y_start, y_end)
|
||||
tile_w = x_end - x_start + 1
|
||||
tile_h = y_end - y_start + 1
|
||||
# 3. 요청 범위를 그대로 덮는 타일 범위를 잡는다(주변으로 넓히지 않는다).
|
||||
# 한 변 타일 수가 한도를 넘으면 zoom을 한 단계씩 낮춘다 — 도엽 1매를 zoom 18로 받으면
|
||||
# 한 변이 19타일(4,864px)이라 파일이 지나치게 커진다(2026-08-01 사용자 지시).
|
||||
zoom = SURFACE_MAP_MAX_ZOOM
|
||||
while True:
|
||||
x1, y1 = latlon_to_tile(lat_max, lon_min, zoom)
|
||||
x2, y2 = latlon_to_tile(lat_min, lon_max, zoom)
|
||||
x_start, x_end = min(x1, x2), max(x1, x2)
|
||||
y_start, y_end = min(y1, y2), max(y1, y2)
|
||||
tile_w = x_end - x_start + 1
|
||||
tile_h = y_end - y_start + 1
|
||||
if zoom <= SURFACE_MAP_MIN_ZOOM:
|
||||
break
|
||||
if max(tile_w, tile_h) <= SURFACE_MAP_MAX_TILES_PER_SIDE:
|
||||
break
|
||||
zoom -= 1
|
||||
|
||||
# 4. 개별 타일 다운로드 및 이미지 병합
|
||||
map_img = Image.new("RGBA", (tile_w * 256, tile_h * 256))
|
||||
|
||||
@@ -516,12 +516,14 @@ 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"
|
||||
|
||||
# 배경 지도(위성·하이브리드·백지도) 확보 범위 = 기준 박스 + 주변 박스.
|
||||
# 배경 지도(위성·하이브리드·백지도) 확보 범위 = 계획노선이 걸치는 **기준 도엽**의 도곽.
|
||||
# 1:5,000 도엽 1매는 약 2.2km × 2.3km다. 주변 도엽은 받지 않는다(2026-08-01 사용자 지시).
|
||||
#
|
||||
# 기준 박스 = 계획노선과 기준 좌표를 덮는 한 장. 그 주위로 같은 크기의 박스를 몇 겹 더 받는다.
|
||||
# 1이면 주변 8장을 더해 3×3 배치가 된다(2026-08-01 사용자 지시).
|
||||
# 해상도(zoom)는 브이월드 기본 스케일 그대로 두고 범위만 넓힌다 — 화소를 키우는 것이 아니다.
|
||||
SURFACE_MAP_NEIGHBOR_RINGS = 1
|
||||
# 도엽 1매를 zoom 18로 받으면 한 변이 19타일(4,864px)이라 파일이 지나치게 커진다.
|
||||
# 아래 한도 안에 들어오는 가장 선명한 zoom을 자동으로 고른다(브이월드 타일 눈금 그대로).
|
||||
SURFACE_MAP_MAX_TILES_PER_SIDE = 16
|
||||
SURFACE_MAP_MAX_ZOOM = 18
|
||||
SURFACE_MAP_MIN_ZOOM = 14
|
||||
|
||||
# 브이월드 지도서비스 도엽 자동 다운로드 — 세션 만료 시 id/pw로 자동 재로그인 (.env)
|
||||
VWORLD_LOGIN_ID = os.getenv("VWORLD_LOGIN_ID", "")
|
||||
|
||||
Reference in New Issue
Block a user