/* ============================================================================= * map_legend.ts * 공용 지도 범례(PLAN 569-2 · 622-2 · 624-2 · 요구 530) — 켠 겹 범례 칸 하나(줄마다 `createLegend`) + 페이지가 붙이는 덧 줄. * 조각 겹 범례(산사태위험 · 국유림 임도 · 경제림 · 생태자연도) = 받을 자료 없음 → 켬끔만 보고 여기서 만듦(`tileLegendRows`). * 받은 자료 범례 줄(수 · 넓이 · 급경사 범위 · 624-3 D03 에서 옮김) = `legendRows(on, data)` — 받은 자료 꼴은 `LegendData` * (D03 옛 캔버스 자료 · B05 GL 자료 둘 다 맞음 · 받는 일은 페이지 몫). * 쓰는 법 = `const legend = createMapLegend("d03-plan__legend")` → `legend.show(rows, [덧 줄])` · 줄 · 덧 없으면 숨김. * ========================================================================== */ import { createLegend, type LegendItem } from "@ui/ui_template_flow_legend"; import { forestLegendRows } from "./map_forest_layers"; import type { PlanBand, PlanForestRoads, PlanLandslideZones, PlanRaster, PlanSoils, PlanZones, } from "./map_layer_api"; import { hectare } from "./map_layer_data"; import { LANDSLIDE_LEGEND, LINE_KEYS, LINE_LAYERS, ZONE_COLORS, type MapLayerKey, } from "./map_layer_list"; import { SLOPE_MAX_PCT, bandRatios, pctToDeg, type SlopeBand } from "./logic/map_slope_bands"; /** 지물 수만 보는 겹(옛 캔버스 자료 · GeoJSON 둘 다). */ interface Counted { features?: ArrayLike; } /** 범례가 읽는 받은 자료 — 받은 겹만. */ export interface LegendData { slope: { raster: PlanRaster; bands: SlopeBand[] }; landslideZones: { info: PlanLandslideZones }; zones: { summary: PlanZones["summary"] }; /** 토양형(코드 차례) 색 값 · 요약. */ soils: { kinds: Array<{ color: string }>; summary: PlanSoils["summary"] }; bands: { info: { stream: PlanBand; road: PlanBand } }; water: Counted; building: Counted; road: { small: Counted }; forestRoadFile: { info: PlanForestRoads }; faults: { count: number; length_m: number }; lineaments: { count: number; length_m: number }; zeroOrder: { count: number; length_m: number }; } export interface MapLegend { root: HTMLElement; show: (rows: LegendItem[][], tail?: HTMLElement[]) => void; } /** 켠 조각 겹 범례 — 산사태위험 다음 산림 셋. */ export const tileLegendRows = (on: Record): LegendItem[][] => [ ...(on.landslide ? [LANDSLIDE_LEGEND] : []), ...forestLegendRows(on), ]; /** `className` = 자리 · 꼴(페이지 CSS). */ export function createMapLegend(className: string): MapLegend { const root = document.createElement("div"); root.className = className; root.title = "켠 겹 범례 — 받은 지형 범위 안 수 · 넓이"; return { root, show(rows, tail = []) { root.replaceChildren(...rows.map((row) => createLegend(row)), ...tail); root.hidden = !rows.length && !tail.length; }, }; } const km = (m: number): string => `${(m / 1000).toFixed(2)} km`; /** 급경사 범례 — 범위마다 「아래~위 %(°) · 비율」(위 = 끝 = 「↑」). */ function slopeRow({ raster, bands }: LegendData["slope"]): LegendItem[] { const ratios = bandRatios(raster.counts ?? [], bands); const deg = (pct: number): string => pctToDeg(pct).toFixed(0); return bands.map((band, i) => { const open = band.high >= SLOPE_MAX_PCT; const range = open ? `${band.low} %↑(${deg(band.low)}°↑)` : `${band.low}~${band.high} %(${deg(band.low)}~${deg(band.high)}°)`; return { label: `${range} · ${(ratios[i] * 100).toFixed(1)} %`, color: `var(${band.color})` }; }); } /** 산사태취약지역 범례 — 점 · 필지 · 위치 없는 시도 · 표 없는 시도 · 지적 못 받음. */ function landslideZoneRow({ info }: LegendData["landslideZones"]): LegendItem[] { const color = "var(--draw-map-landslide-zone)"; const items: LegendItem[] = [ { label: `산사태취약지역 점 ${info.points.length}곳`, color, kind: "dot", off: !info.points.length, }, { label: `지번 맞는 필지 ${info.parcels.features?.length ?? 0}곳(테두리)`, color, kind: "dash", off: !info.parcels.features?.length, }, ]; for (const sido of info.sidos) if (!sido.located) items.push({ label: `${sido.name} 위치 없음 — 필지만`, color, off: true }); for (const name of info.missing) items.push({ label: `${name} — 이 시도 자료 없음`, color: "transparent", off: true }); if (!info.cadastral) items.push({ label: "지적도 못 받음 — 필지 테두리 없음", color: "transparent", off: true }); return items; } /** 켠 겹의 받은 자료 범례 줄(급경사 · 조각 겹 · 산사태취약 · 보호구역 · 토양 · 띠 · 선 겹 · 물 면 · 건물 · 임도). */ export function legendRows( on: Record, data: Partial, ): LegendItem[][] { const rows: LegendItem[][] = []; if (on.slope && data.slope) rows.push(slopeRow(data.slope)); rows.push(...tileLegendRows(on)); // 조각 겹 = 받기 없음(622-2 · 공용 범례) if (on.landslideZones && data.landslideZones) rows.push(landslideZoneRow(data.landslideZones)); if (on.zones && data.zones) { const items = Object.entries(data.zones.summary) .filter(([, kind]) => kind.count || kind.failed) .map(([key, kind]) => ({ label: kind.failed ? `${kind.label} 받지 못함` : `${kind.label} ${kind.count}곳 ${hectare(kind.area_m2)}`, color: `var(${ZONE_COLORS[key]})`, off: !!kind.failed, })); rows.push( items.length ? items : [{ label: "보호 · 규제 구역 없음(받은 범위 안)", color: "transparent", off: true }], ); } if (on.soils && data.soils) { const { kinds, summary } = data.soils; rows.push( summary.length ? summary.map((soil, i) => ({ label: `${soil.name} ${hectare(soil.area_m2)}`, color: `var(${kinds[i].color})`, })) : [{ label: "산림입지도 없음(받은 범위 안)", color: "transparent", off: true }], ); } if (on.bands && data.bands) { const { stream, road } = data.bands.info; rows.push([ { label: `계류 ${stream.dist_m} m 띠 ${hectare(stream.area_m2)} (${(stream.ratio * 100).toFixed(1)} %)`, color: "var(--draw-map-band-stream)", }, { label: `도로 ${road.dist_m} m 띠 ${hectare(road.area_m2)} (${(road.ratio * 100).toFixed(1)} %)`, color: "var(--draw-map-band-road)", }, ]); } const lines = LINE_KEYS.flatMap((key) => { const got = on[key] && data[key]; if (!got) return []; const { label, color, dash } = LINE_LAYERS[key]; return [ { label: `${label} ${got.count}가닥 ${km(got.length_m)}`, color: `var(${color})`, kind: dash.length ? ("dash" as const) : ("line" as const), off: !got.count, }, ]; }); if (lines.length) rows.push(lines); const tail: LegendItem[] = []; if (on.water && data.water) tail.push({ label: `물 면 ${data.water.features?.length ?? 0}곳`, color: "var(--draw-map-water)", }); if (on.building && data.building) tail.push({ label: `건물 ${(data.building.features?.length ?? 0).toLocaleString()}동`, color: "var(--draw-map-building)", }); if (on.forestRoad && data.road) tail.push({ label: `임도 짐작 ${data.road.small.features?.length ?? 0}가닥`, color: "var(--draw-map-forest-road)", kind: "dash", }); if (on.forestRoadFile && data.forestRoadFile) tail.push({ label: `올린 임도망도 ${data.forestRoadFile.info.count}가닥 ${km(data.forestRoadFile.info.length_m)}`, color: "var(--draw-map-forest-road-file)", kind: "line", }); if (tail.length) rows.push(tail); return rows; }