diff --git a/B04_wf1_Surface/B04_wf1_Surface_Engine_VWorld.py b/B04_wf1_Surface/B04_wf1_Surface_Engine_VWorld.py index d4fad938..23f98c64 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_Engine_VWorld.py +++ b/B04_wf1_Surface/B04_wf1_Surface_Engine_VWorld.py @@ -19,10 +19,10 @@ try: VWORLD_API_KEY = getattr( config_system, "VWORLD_API_KEY", "3DBD7306-7DBD-38BB-B292-267C5ED7AC6B" ) - SURFACE_MAP_MARGIN_TILES = getattr(config_system, "SURFACE_MAP_MARGIN_TILES", 3) + SURFACE_MAP_NEIGHBOR_RINGS = getattr(config_system, "SURFACE_MAP_NEIGHBOR_RINGS", 1) except ImportError: VWORLD_API_KEY = "3DBD7306-7DBD-38BB-B292-267C5ED7AC6B" - SURFACE_MAP_MARGIN_TILES = 3 + SURFACE_MAP_NEIGHBOR_RINGS = 1 def get_epsg_from_prj(prj_content: str) -> str: @@ -105,24 +105,32 @@ def download_vworld_satellite_map( x1, y1 = latlon_to_tile(lat_max, lon_min, zoom) x2, y2 = latlon_to_tile(lat_min, lon_max, zoom) - # 범위를 덮는 타일 바깥으로 주변 셀을 더 받는다(해상도는 그대로, 셀만 확장). - margin = SURFACE_MAP_MARGIN_TILES - x_start = min(x1, x2) - margin - x_end = max(x1, x2) + margin - y_start = min(y1, y2) - margin - y_end = max(y1, y2) + margin + # 기준 박스 = 계획노선·기준 좌표를 덮는 한 장. 그 주위로 같은 크기의 박스를 더 받는다. + # 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 - # 너무 많은 타일을 다운로드하여 IP 차단되는 것을 방지 - if tile_w > 15: - tile_w = 15 - x_end = x_start + 14 - if tile_h > 15: - tile_h = 15 - y_end = y_start + 14 - # 4. 개별 타일 다운로드 및 이미지 병합 map_img = Image.new("RGBA", (tile_w * 256, tile_h * 256)) headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"} diff --git a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts index 4a1788a8..cb05e2ce 100644 --- a/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts +++ b/B04_wf1_Surface/B04_wf1_Surface_UI_MapViewer.ts @@ -147,7 +147,6 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { root.append(header, viewport); let currentProjectId: string | null = null; - let referenceBounds: SurfaceBounds | null = null; let meta: VWorldMeta | null = null; // 배수유역 오버레이가 lon/lat을 화면 좌표로 옮길 때 쓴다. 레이어 로드 시 1회 만든다. let normalizer: Normalizer | null = null; @@ -268,29 +267,19 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { scheduleDraw(); } - function fitReferenceBounds(): void { - if (!meta || !referenceBounds) return; - const rect = viewport.getBoundingClientRect(); - const width = Math.max(rect.width, 1); - const height = Math.max(rect.height, 1); - const mapRect = computeMapRect(meta, width, height); - const referenceWidth = Math.max(referenceBounds.x_max - referenceBounds.x_min, 1); - const referenceHeight = Math.max(referenceBounds.y_max - referenceBounds.y_min, 1); - scale = - Math.min(meta.width_meters / referenceWidth, meta.height_meters / referenceHeight) * 0.9; - const centerX = (referenceBounds.x_min + referenceBounds.x_max) / 2; - const centerY = (referenceBounds.y_min + referenceBounds.y_max) / 2; - const baseX = mapRect.x + ((centerX - meta.x_min) / meta.width_meters) * mapRect.width; - const baseY = mapRect.y + (1 - (centerY - meta.y_min) / meta.height_meters) * mapRect.height; - offsetX = -(baseX - width / 2) * scale; - offsetY = -(baseY - height / 2) * scale; - } - - function resetView(): void { + /** 확보한 배경 지도(브이월드) 전체가 보이도록 맞춘다. + * + * 3D(라이다)와 2D(브이월드)는 다루는 범위가 다르다 — 라이다는 노선 주변의 좁은 구역, + * 2D 지도는 그보다 훨씬 넓은 주변까지 담는다. 이전에는 2D 지도를 라이다 범위에 맞춰 + * 확대해서, 배경을 넓게 받아도 화면에 보이는 범위가 늘 같았다(2026-08-01 사용자 지적). */ + function fitMapExtent(): void { scale = 1; offsetX = 0; offsetY = 0; - fitReferenceBounds(); + } + + function resetView(): void { + fitMapExtent(); updateImageTransform(); scheduleDraw(); } @@ -475,11 +464,12 @@ export function createSurfaceMapViewer(): SurfaceMapViewer { return { root, - render(projectId, nextReferenceBounds) { + // 라이다 범위는 더 이상 화면 배율에 쓰지 않는다(2D는 브이월드 범위 기준). + // 인자는 호출측 호환을 위해 유지한다. + render(projectId) { currentProjectId = projectId; watershed.reset(); watershed.setProject(projectId); - referenceBounds = nextReferenceBounds ?? null; void loadLayers(); }, dispose() { diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts index bdaf06b6..80a75dde 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts @@ -475,35 +475,14 @@ export function createDrainagePanel(): DrainagePanel { boundaryEditor.setEditMode(boundaryMode); }); - /** 노선 전체가 보이도록 배율·중심을 맞춘다. 노선이 없으면 도엽 전체를 그대로 보여준다. */ + /** 확보한 배경 지도(브이월드) 전체가 보이도록 맞춘다. + * + * 노선 범위에 맞춰 확대하면 배경을 넓게 받아도 화면에 보이는 범위가 늘 같았다 + * (2026-08-01 사용자 지적). 노선 주변 지형까지 함께 보고 판단하도록 배경 전체를 띄운다. */ function fitToRoute(): void { scale = 1; offsetX = 0; offsetY = 0; - if (!meta || routePoints.length < 2) return; - const rect = viewport.getBoundingClientRect(); - const width = Math.max(rect.width, 1); - const height = Math.max(rect.height, 1); - const mapRect = computeMapRect(meta, width, height); - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - routePoints.forEach((point) => { - if (point.x < minX) minX = point.x; - if (point.x > maxX) maxX = point.x; - if (point.y < minY) minY = point.y; - if (point.y > maxY) maxY = point.y; - }); - const routeWidth = Math.max(maxX - minX, 1); - const routeHeight = Math.max(maxY - minY, 1); - scale = Math.min(meta.width_meters / routeWidth, meta.height_meters / routeHeight) * 0.85; - const centerX = (minX + maxX) / 2; - const centerY = (minY + maxY) / 2; - const baseX = mapRect.x + ((centerX - meta.x_min) / meta.width_meters) * mapRect.width; - const baseY = mapRect.y + (1 - (centerY - meta.y_min) / meta.height_meters) * mapRect.height; - offsetX = -(baseX - width / 2) * scale; - offsetY = -(baseY - height / 2) * scale; } async function loadLayers(): Promise { diff --git a/config/config_system.py b/config/config_system.py index 456f314f..a2e356e1 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -516,11 +516,12 @@ 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" -# 배경 지도(위성·하이브리드·백지도)를 내려받을 때 덧붙일 주변 셀(타일) 겹 수. -# 계획노선과 기준 좌표(라이다 범위)를 덮는 타일을 먼저 정하고, 그 바깥으로 이만큼 더 받는다. -# 해상도(zoom)는 그대로 두고 주변 셀만 늘린다 — 브이월드가 더 높은 해상도를 주지 않는다 -# (2026-08-01 사용자 지시). zoom 18에서 셀 1겹 ≈ 122m(위도 37° 기준), 3겹 ≈ 366m. -SURFACE_MAP_MARGIN_TILES = 3 +# 배경 지도(위성·하이브리드·백지도) 확보 범위 = 기준 박스 + 주변 박스. +# +# 기준 박스 = 계획노선과 기준 좌표를 덮는 한 장. 그 주위로 같은 크기의 박스를 몇 겹 더 받는다. +# 1이면 주변 8장을 더해 3×3 배치가 된다(2026-08-01 사용자 지시). +# 해상도(zoom)는 브이월드 기본 스케일 그대로 두고 범위만 넓힌다 — 화소를 키우는 것이 아니다. +SURFACE_MAP_NEIGHBOR_RINGS = 1 # 브이월드 지도서비스 도엽 자동 다운로드 — 세션 만료 시 id/pw로 자동 재로그인 (.env) VWORLD_LOGIN_ID = os.getenv("VWORLD_LOGIN_ID", "")