/* ============================================================================= * E07_StabArea_Raster.ts * E07 광역 위험 지도 화면 계산(PLAN 536-2) — 저장본 격자 풀기 · 등급 · 안전율 색 · RGBA · 셀 찾기. * 격자 꼴 = 서버 `E07_StabArea_Router.py` 머리 주석(fs · si uint16 ×1000 · cls uint8 · slope ×10 · * m ×100 · 작은 끝 base64 · 행 0 = 남쪽). 순수 함수만(DOM · fetch 금지). * ========================================================================== */ export interface Grid { x0: number; y0: number; dx: number; nx: number; ny: number; } export type LevelKey = "low" | "mid" | "high"; export interface StationRow { chainage_m: number; label: string | null; cells: number; fs_min?: number; si_min?: number; worst_class?: number; risk_area_m2?: number; risk_ratio?: number; level?: LevelKey; slope_max_deg?: number; } export interface AreaSummary { areas_m2: Record; band_area_m2: number; covered_area_m2: number; risk_area_m2: number; fs_min: number | null; station_levels: Record; stations: number; } export interface AreaResult { stale?: boolean; computed_at: string; elapsed_s: number; settings: Record; grid: Grid; fs: string; si: string; cls: string; slope: string; m: string; stations: StationRow[]; summary: AreaSummary; } /** 풀어 둔 격자 — 자료 없음 = NaN(fs · si · slope · m) · 0(cls) */ export interface Decoded { grid: Grid; fs: Float32Array; si: Float32Array; cls: Uint8Array; slope: Float32Array; m: Float32Array; } /** 등급 열쇠(서버 CLASS_KEYS 차례 · 1~6) */ export const CLASS_KEYS = ["stable", "moderate", "quasi", "lower", "upper", "defended"] as const; export const RISK_CLASS = 4; /** 측점 등급(서버 LEVEL_KEYS 차례) · 색 */ export const LEVEL_KEYS: readonly LevelKey[] = ["low", "mid", "high"]; export const LEVEL_COLORS: Record = { low: "#1a9850", mid: "#fee08b", high: "#d73027", }; /** 등급 색 1~6 — SINMAP 관례(초록 → 빨강) */ export const CLASS_COLORS = ["#1a9850", "#91cf60", "#d9ef8b", "#fee08b", "#fc8d59", "#d73027"]; /** 안전율 색 단계 — 1.0 · 1.2 · 1.5 경계 */ export const FS_STEPS: readonly { below: number; color: string }[] = [ { below: 1.0, color: "#a50026" }, { below: 1.2, color: "#f46d43" }, { below: 1.5, color: "#fee08b" }, { below: 2.0, color: "#a6d96a" }, { below: Infinity, color: "#1a9850" }, ]; function bytes(text: string): Uint8Array { const raw = atob(text); const out = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i += 1) out[i] = raw.charCodeAt(i); return out; } function u16(text: string, scale: number): Float32Array { const b = bytes(text); const view = new DataView(b.buffer); const out = new Float32Array(b.length / 2); for (let i = 0; i < out.length; i += 1) { const v = view.getUint16(i * 2, true); out[i] = v === 65535 ? NaN : v / scale; } return out; } export function decode(result: AreaResult): Decoded { const m = bytes(result.m); return { grid: result.grid, fs: u16(result.fs, 1000), si: u16(result.si, 1000), cls: bytes(result.cls), slope: u16(result.slope, 10), m: Float32Array.from(m, (v) => (v === 255 ? NaN : v / 100)), }; } function rgb(hex: string): [number, number, number] { const n = Number.parseInt(hex.slice(1), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; } export function fsColor(value: number): string { return (FS_STEPS.find((s) => value < s.below) ?? FS_STEPS[FS_STEPS.length - 1]).color; } /** 격자 → RGBA(줄 0 = 북쪽 끝 · 캔버스 그림용) · 자료 없음 = 투명 */ export function rasterRgba(data: Decoded, layer: "cls" | "fs"): Uint8ClampedArray { const { nx, ny } = data.grid; const out = new Uint8ClampedArray(nx * ny * 4); const classRgb = CLASS_COLORS.map(rgb); const fsRgb = FS_STEPS.map((s) => rgb(s.color)); for (let iy = 0; iy < ny; iy += 1) { const row = ny - 1 - iy; for (let ix = 0; ix < nx; ix += 1) { const i = iy * nx + ix; const k = data.cls[i]; if (!k) continue; const j = FS_STEPS.findIndex((s) => data.fs[i] < s.below); const color = layer === "cls" ? classRgb[k - 1] : fsRgb[j < 0 ? fsRgb.length - 1 : j]; const at = (row * nx + ix) * 4; out[at] = color[0]; out[at + 1] = color[1]; out[at + 2] = color[2]; out[at + 3] = 255; } } return out; } /** 모델 좌표 → 셀 번호(격자 밖 = -1) */ export function cellIndex(grid: Grid, x: number, y: number): number { const ix = Math.round((x - grid.x0) / grid.dx); const iy = Math.round((y - grid.y0) / grid.dx); return ix < 0 || iy < 0 || ix >= grid.nx || iy >= grid.ny ? -1 : iy * grid.nx + ix; } /** 등급별 셀 수(1~6) — 범례 · 검증용 */ export function classCounts(data: Decoded): number[] { const out = [0, 0, 0, 0, 0, 0]; for (const k of data.cls) if (k) out[k - 1] += 1; return out; }