Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
83 lines
3.7 KiB
TypeScript
83 lines
3.7 KiB
TypeScript
/* =============================================================================
|
||
* common_util_surface_sampler.ts
|
||
* 확정 지표면(DTM 격자) 표고를 브라우저에서 바로 읽는다 — 노선을 고치는 동안 서버 왕복 없이.
|
||
*
|
||
* ⚠ 파이썬·TS 짝 — `common_util/common_util_surface_sampler.py` 의 `DtmGridSampler.sample_xy`
|
||
* (scipy `RegularGridInterpolator` linear + 네 칸 유효) · 격자 꼴 = `dtm_grid_bytes`.
|
||
* 한쪽을 고치면 다른 쪽도 고치고 거울 시험 `resources/tester/test_344_1_surface_sampler_mirror.py`
|
||
* 를 돌림.
|
||
*
|
||
* 칸 폭이 고르지 않음(축이 float32 로 저장돼 0.97~1.09 m) — 「(x − x0) / 1 m」 셈 금지 · 축 배열을
|
||
* 이진 탐색(`searchsorted(side="right") − 1`)해 서버와 같은 칸을 봄.
|
||
* ========================================================================== */
|
||
|
||
/** 오름차순 축 · 행렬 · 유효 비트 — 서버 `dtm_grid_bytes` 를 그대로 푼 것. */
|
||
export interface DtmGrid {
|
||
x: Float64Array;
|
||
y: Float64Array;
|
||
/** 행 = y · 열 = x (`z[iy * nx + ix]`). */
|
||
z: Float32Array | Float64Array;
|
||
/** 칸마다 한 비트(행 우선 · 낮은 비트 먼저). */
|
||
validBits: Uint8Array;
|
||
}
|
||
|
||
const GRID_MAGIC = "AGRD";
|
||
const GRID_VERSION = 1;
|
||
const HEAD_BYTES = 24;
|
||
|
||
/** 서버 `dtm_grid_bytes` 이진을 푼다 — 꼴이 다르면 throw. */
|
||
export function parseDtmGrid(buffer: ArrayBuffer): DtmGrid {
|
||
const magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
|
||
const [version, nx, ny, zBytes] = new Uint32Array(buffer, 4, 4);
|
||
if (magic !== GRID_MAGIC || version !== GRID_VERSION || (zBytes !== 4 && zBytes !== 8)) {
|
||
throw new Error("지형 격자 꼴이 다름");
|
||
}
|
||
const x = new Float64Array(buffer, HEAD_BYTES, nx);
|
||
const y = new Float64Array(buffer, HEAD_BYTES + nx * 8, ny);
|
||
const zStart = HEAD_BYTES + (nx + ny) * 8;
|
||
const z =
|
||
zBytes === 4
|
||
? new Float32Array(buffer, zStart, nx * ny)
|
||
: new Float64Array(buffer, zStart, nx * ny);
|
||
const validBits = new Uint8Array(buffer, zStart + nx * ny * zBytes, Math.ceil((nx * ny) / 8));
|
||
return { x, y, z, validBits };
|
||
}
|
||
|
||
/** `np.searchsorted(axis, value, side="right") - 1` — value 이하인 마지막 칸 번호 · 앞이면 −1. */
|
||
function cellIndex(axis: Float64Array, value: number): number {
|
||
if (!(value >= axis[0])) return -1; // NaN 도 여기(밖)
|
||
let low = 0;
|
||
let high = axis.length;
|
||
while (high - low > 1) {
|
||
const mid = (low + high) >> 1;
|
||
if (axis[mid] <= value) low = mid;
|
||
else high = mid;
|
||
}
|
||
return low;
|
||
}
|
||
|
||
function isValid(grid: DtmGrid, index: number): boolean {
|
||
return ((grid.validBits[index >> 3] >> (index & 7)) & 1) === 1;
|
||
}
|
||
|
||
/** (x, y) 표고(m) — 네 칸이 모두 유효할 때만 값 · 밖 · 빈칸 · 마지막 선 위는 NaN(임의 표고로 안 메움).
|
||
* 식 · 셈 차례 = scipy `evaluate_linear_2d`(같은 IEEE 연산 → 같은 값). */
|
||
export function heightAt(grid: DtmGrid, x: number, y: number): number {
|
||
const nx = grid.x.length;
|
||
const ix = cellIndex(grid.x, x);
|
||
const iy = cellIndex(grid.y, y);
|
||
if (ix < 0 || iy < 0 || ix >= nx - 1 || iy >= grid.y.length - 1) return NaN;
|
||
const i00 = iy * nx + ix;
|
||
const i10 = i00 + nx;
|
||
if (!isValid(grid, i00) || !isValid(grid, i00 + 1)) return NaN;
|
||
if (!isValid(grid, i10) || !isValid(grid, i10 + 1)) return NaN;
|
||
const ty = (y - grid.y[iy]) / (grid.y[iy + 1] - grid.y[iy]);
|
||
const tx = (x - grid.x[ix]) / (grid.x[ix + 1] - grid.x[ix]);
|
||
let value = 0.0;
|
||
value = value + grid.z[i00] * (1 - ty) * (1 - tx);
|
||
value = value + grid.z[i00 + 1] * (1 - ty) * tx;
|
||
value = value + grid.z[i10] * ty * (1 - tx);
|
||
value = value + grid.z[i10 + 1] * ty * tx;
|
||
return Number.isFinite(value) ? value : NaN;
|
||
}
|