- 서버: 15 · 35 · 40° 단계 PNG 길을 걷고 기울기 % 값 그림 한 장(칸 = % 내림 0~250 · 빈 칸 255) + 1 % 칸 수 · 파생 저장본 판 2(열쇠 = 격자 지문) - 화면: 고른 범위대로 값 그림을 칠함(서버 왕복 없음) · 범례 비율 = 칸 수로 바로 셈 · 범례 급경사 줄 ⚙ → 모달(아래 % · 위 % · 색 · ° 환산 · 범위 추가 · 지움 · 기본값 · 빈 값 · 겹침 막음) - 기본 = 지식DB 15 · 35 · 40° 의 % 26.8 · 70.0 · 83.9 · 기억 = 화면 취향 aislo:pref:d03.slope-bands(계정) - 시험: 569_1 급경사 시험을 값 그림 · 칸 수로 고침, 581_1 은 bands 대신 counts(옛 단계 응답이 없어짐) · 새 603 시험(칸 수 합 = 유효 칸 · 기본 비율 = 옛 15 · 35 · 40° 비율) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
4.3 KiB
TypeScript
99 lines
4.3 KiB
TypeScript
/* =============================================================================
|
||
* D03_PlanAnalysis_SlopeBands.ts
|
||
* D03 급경사 범위(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`.
|
||
* 부르는 곳 = `D03_PlanAnalysis_UI_SlopeBands.ts` · `D03_PlanAnalysis_UI_LayerLegend.ts`.
|
||
* ========================================================================== */
|
||
|
||
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<SlopeBand>) => ({
|
||
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);
|
||
}
|