/* ============================================================================= * B05_Profile_UI_Drainage_ZoomRange.ts * 배수유역 지도의 줌아웃 한계(가장 작은 배율) — 도엽 전체가 아니라 **노선 기준**(계획서 281). * * 한계 = 노선 범위를 가운데 두고 ZOOM_OUT_ROUTE_FACTOR 배로 넓힌 범위가 화면에 들어오는 배율. * 유역 폴리곤이 그 밖으로 나가면 유역까지 넓힌다. 처음 보기(노선 + 여유)보다 좁히지 않고, * 도엽 전체(배율 1)보다 멀리는 안 간다. 노선이 없으면 종전 꼴(ZOOM_OUT_FALLBACK_SCALE). * 화면 그리기 없는 순수 셈 — 시험이 Node 로 바로 돌린다. * ========================================================================== */ import type { DetailBasin, VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { computeMapRect, computeRouteView, createNormalizer, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; /** 노선 범위를 몇 배까지 멀리 볼지 — 키우면 더 멀리 빠진다(사용자가 바꿀 수 있는 값). */ export const ZOOM_OUT_ROUTE_FACTOR = 2; /** 노선이 없을 때의 줌아웃 한계 — 도엽 전체의 절반까지(종전 값). */ export const ZOOM_OUT_FALLBACK_SCALE = 0.5; /** 줌아웃 한계 배율. 지도 메타가 없거나 노선이 두 점 미만이면 종전 값. */ export function computeZoomOutLimit( meta: VWorldMeta | null, routePoints: ReadonlyArray<{ x: number; y: number }>, basins: ReadonlyArray>, width: number, height: number, factor: number = ZOOM_OUT_ROUTE_FACTOR, ): number { if (!meta || routePoints.length < 2 || width <= 0 || height <= 0) { return ZOOM_OUT_FALLBACK_SCALE; } const route = { x_min: Math.min(...routePoints.map((point) => point.x)), x_max: Math.max(...routePoints.map((point) => point.x)), y_min: Math.min(...routePoints.map((point) => point.y)), y_max: Math.max(...routePoints.map((point) => point.y)), }; // 도엽 정규 좌표(0~1, 위가 0)로 모아 잰다 — 노선(m)과 유역(경위도)을 한 자로 본다. const spanX = meta.width_meters || 1; const spanY = meta.height_meters || 1; const centerX = ((route.x_min + route.x_max) / 2 - meta.x_min) / spanX; const centerY = 1 - ((route.y_min + route.y_max) / 2 - meta.y_min) / spanY; const halfW = ((route.x_max - route.x_min) / spanX) * (factor / 2); const halfH = ((route.y_max - route.y_min) / spanY) * (factor / 2); let x0 = centerX - halfW; let x1 = centerX + halfW; let y0 = centerY - halfH; let y1 = centerY + halfH; const normalizer = createNormalizer(meta); basins.forEach((basin) => basin.polygon_lonlat.forEach(([lon, lat]) => { const nx = (lon - normalizer.lonMin) / normalizer.lonRange; const ny = 1 - (lat - normalizer.latMin) / normalizer.latRange; x0 = Math.min(x0, nx); x1 = Math.max(x1, nx); y0 = Math.min(y0, ny); y1 = Math.max(y1, ny); }), ); // 배율 s 에서 화면에 드는 정규 폭 = 화면 폭 / (지도 사각형 폭 · s). const mapRect = computeMapRect(meta, width, height); const fit = Math.min( width / (mapRect.width * Math.max(x1 - x0, 1e-9)), height / (mapRect.height * Math.max(y1 - y0, 1e-9)), ); // 처음 보기(노선 + 여유 200m)는 늘 한계 안 — 짧은 노선에서 처음 화면보다 좁아지지 않게. const initial = computeRouteView(meta, route, width, height).scale; return Math.max(1, Math.min(fit, initial)); } /** 휠 한 번의 새 배율 — 한계 안에서 멈춘다. 이미 한계 밖이면 더 빠지지만 않게(튀지 않게) 둔다. */ export function clampWheelScale( next: number, previous: number, minScale: number, maxScale: number, ): number { return Math.min(maxScale, Math.max(Math.min(minScale, previous), next)); }