Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_ZoomRange.ts
T

84 lines
3.8 KiB
TypeScript

/* =============================================================================
* 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<Pick<DetailBasin, "polygon_lonlat">>,
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));
}