/* ============================================================================= * map_slope_bands.ts(logic) * 공용 지도 급경사 범위(624-3 공용으로 옮김)(PLAN 603 · 요구 466 · 467 · 468) — 기울기 % 범위 · 색 · 비율 셈(화면 · 서버 없이). * 서버 값 그림 칸 = 기울기 % 내림(0~250 · 250 = 250 % 이상 · `D03_PlanAnalysis_Layers.slope_values_png`) · * 칸 v 는 가운데(v + 0.5)가 [아래, 위) 안이면 그 범위(위 = 250 이면 250 % 이상도) · 앞 범위 먼저. * 기본 = 지식DB 15 · 35 · 40° 의 % — `임도망_편성.md:20` · `타당성평가.md:59` · `노선_배치.md:29`. * 부르는 곳 = `map_slope_bands.ts` · `map_legend.ts` · D03 `UI_LayerLegend`. * ========================================================================== */ export interface SlopeBand { /** 아래 % (포함) */ low: number; /** 위 % (뺌 · 250 = 끝까지) */ high: number; /** 그림 색 이름(`--draw-map-*`) — `SLOPE_COLORS` 중 하나 */ color: string; } export const SLOPE_MAX_PCT = 250; export const SLOPE_EMPTY = 255; export const SLOPE_MAX_BANDS = 8; /** 고를 색(이름 · 그림 색 값). */ export const SLOPE_COLORS: Array<{ name: string; color: string }> = [ { name: "노랑", color: "--draw-map-landslide-3" }, { name: "주황", color: "--draw-map-landslide-2" }, { name: "빨강", color: "--draw-map-landslide-1" }, { name: "연두", color: "--draw-map-landslide-4" }, { name: "초록", color: "--draw-map-landslide-5" }, { name: "하늘", color: "--draw-map-stream" }, { name: "파랑", color: "--draw-map-zone-water" }, { name: "보라", color: "--draw-map-zone-heritage" }, { name: "분홍", color: "--draw-map-parcel" }, ]; export const DEFAULT_SLOPE_BANDS: SlopeBand[] = [ { low: 26.8, high: 70.0, color: "--draw-map-landslide-3" }, { low: 70.0, high: 83.9, color: "--draw-map-landslide-2" }, { low: 83.9, high: SLOPE_MAX_PCT, color: "--draw-map-landslide-1" }, ]; export const pctToDeg = (pct: number): number => (Math.atan(pct / 100) * 180) / Math.PI; /** 범위 고칠 거리 — 없으면 null(빈 값 · 0~250 밖 · 아래 ≥ 위 · 겹침 · 없음 · 너무 많음). */ export function checkBands(bands: SlopeBand[]): string | null { if (!bands.length) return "범위가 하나는 있어야 합니다"; if (bands.length > SLOPE_MAX_BANDS) return `범위는 ${SLOPE_MAX_BANDS}개까지입니다`; for (const [i, band] of bands.entries()) { const at = `${i + 1}번째 범위`; if (!Number.isFinite(band.low) || !Number.isFinite(band.high)) return `${at} — 값이 비었습니다`; if (band.low < 0 || band.high > SLOPE_MAX_PCT) return `${at} — 0~${SLOPE_MAX_PCT} % 안으로`; if (band.low >= band.high) return `${at} — 아래가 위보다 작아야 합니다`; if (!SLOPE_COLORS.some((c) => c.color === band.color)) return `${at} — 색을 고르세요`; } const sorted = [...bands].sort((a, b) => a.low - b.low); for (let i = 1; i < sorted.length; i++) if (sorted[i].low < sorted[i - 1].high) return `범위가 겹칩니다 — ${sorted[i - 1].low}~${sorted[i - 1].high} % 와 ${sorted[i].low}~${sorted[i].high} %`; return null; } /** 저장 글 → 범위(깨졌거나 맞지 않으면 기본). */ export function parseBands(raw: string | null): SlopeBand[] { try { const got = JSON.parse(raw ?? "null") as unknown; if (Array.isArray(got)) { const bands = got.map((b: Partial) => ({ low: Number(b?.low), high: Number(b?.high), color: String(b?.color), })); if (!checkBands(bands)) return bands; } } catch { // 깨진 값 = 기본 } return DEFAULT_SLOPE_BANDS.map((b) => ({ ...b })); } /** 칸 값 v(0~255) → 범위 차례(없으면 −1). */ export function bandOf(v: number, bands: SlopeBand[]): number { if (v > SLOPE_MAX_PCT) return -1; const mid = v + 0.5; return bands.findIndex( (b) => b.low <= mid && (mid < b.high || (b.high >= SLOPE_MAX_PCT && v === SLOPE_MAX_PCT)), ); } /** 1 % 칸 수 → 범위별 비율(유효 칸 대비). */ export function bandRatios(counts: number[], bands: SlopeBand[]): number[] { const total = counts.reduce((sum, n) => sum + n, 0) || 1; const out = bands.map(() => 0); counts.forEach((n, v) => { const i = bandOf(v, bands); if (i >= 0) out[i] += n; }); return out.map((n) => n / total); }