Files
Aislo/E07_StabArea/logic/E07_StabArea_Raster.ts
T

165 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<string, number>;
band_area_m2: number;
covered_area_m2: number;
risk_area_m2: number;
fs_min: number | null;
station_levels: Record<LevelKey, number>;
stations: number;
}
export interface AreaResult {
stale?: boolean;
computed_at: string;
elapsed_s: number;
settings: Record<string, unknown>;
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<LevelKey, string> = {
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<ArrayBuffer> {
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;
}