fix(B04/B05): 2D 지도 기준을 라이다에서 브이월드 범위로 + 배경 확보를 기준 박스 3x3으로

3D(라이다)와 2D(브이월드)는 다루는 범위가 다르다. 그런데 2D 지도가 열릴 때 라이다
범위에 맞춰 확대하고 있어서, 배경을 넓게 받아도 화면에 보이는 범위가 늘 같았다
(2026-08-01 사용자 지적).

- 확보 범위: 계획노선·기준 좌표를 덮는 기준 박스 + 주변 박스(SURFACE_MAP_NEIGHBOR_RINGS=1)
  = 3x3 배치. 미터/타일 여유폭 방식(SURFACE_MAP_MARGIN_TILES) 폐기.
  해상도(zoom 18)는 브이월드 기본 스케일 그대로 — 화소를 키우는 것이 아니다.
- 타일 상한 15는 양쪽에서 균등하게 줄여 기준 박스가 가장자리로 밀리지 않게 한다.
- B04 하단 지도·B05 배수유역도는 확보한 배경 전체가 보이도록 열린다.
- 표본 실측: 기준 박스 372x401m -> 12x15 타일 = 1463x1828m (이전 731x853m).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 12:35:51 +09:00
co-authored by Claude Opus 5
parent 6af50811b6
commit a17b95f621
4 changed files with 47 additions and 69 deletions
@@ -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)"}
+13 -23
View File
@@ -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() {
@@ -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<void> {
+6 -5
View File
@@ -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", "")