/* ============================================================================= * D03_PlanAnalysis_UI_LayerLegend.ts * D03 바탕 겹 받은 자료 꼴 · 범례 줄 · 겹별 수(PLAN 569-2 · 581 · 584-1 · 584-3) — 켠 겹의 받은 범위 안 수 · 넓이. * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기). * ========================================================================== */ import type { LegendItem } from "@ui/ui_template_flow_legend"; import type { PreparedLayer } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import type { PlanBand, PlanForestRoads, PlanLandslideZones, PlanRaster, PlanSoils, PlanZones, } from "./D03_PlanAnalysis_Api"; import { LANDSLIDE_LEGEND, LINE_KEYS, LINE_LAYERS, ZONE_COLORS, hectare, type LineLayerKey, type PlanLayerKey, } from "./D03_PlanAnalysis_UI_LayerDefs"; /** 선 겹 — 그릴 선 · 받은 범위 안 가닥 · 길이 m. */ export interface PreparedLines { layer: PreparedLayer; count: number; length_m: number; } export type Prepared = Record & { contour: PreparedLayer; /** 세류 · 하천(소하천 · 지방하천 · 연결선). */ stream: [PreparedLayer, PreparedLayer]; cadastral: PreparedLayer; /** 전체 · 그중 임도 짐작(소로 · 미분류) — `road` · `forestRoad` 가 함께 씀. */ road: { all: PreparedLayer; small: PreparedLayer }; /** 올린 임도망도(584-3). */ forestRoadFile: { layer: PreparedLayer; info: PlanForestRoads }; slope: { raster: PlanRaster; image: HTMLImageElement }; hillshade: { raster: PlanRaster; image: HTMLImageElement }; landslide: { raster: PlanRaster; image: HTMLImageElement }; /** 산사태취약지역(584-3) — 점(경위도) · 지번 맞는 필지 테두리. */ landslideZones: { parcels: PreparedLayer; info: PlanLandslideZones }; water: PreparedLayer; zones: { kinds: Map; summary: PlanZones["summary"] }; /** 토양형(코드 차례) → 지물 · 색 값. */ soils: { kinds: Array<{ layer: PreparedLayer; color: string }>; summary: PlanSoils["summary"] }; bands: { stream: PreparedLayer; road: PreparedLayer; info: { stream: PlanBand; road: PlanBand } }; building: PreparedLayer; place: Array<{ lon: number; lat: number; name: string }>; }; const km = (m: number): string => `${(m / 1000).toFixed(2)} km`; /** 산사태취약지역 범례 — 점 · 필지 · 위치 없는 시도 · 표 없는 시도 · 지적 못 받음. */ function landslideZoneRow({ info }: Prepared["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( data.slope.raster.bands.map((band) => ({ label: `${band.min_deg}${band.max_deg < 90 ? `~${band.max_deg}°` : "°↑"} ${(band.ratio * 100).toFixed(1)} %`, color: band.color, })), ); if (on.landslide && data.landslide) rows.push(LANDSLIDE_LEGEND); 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}곳`, color: "var(--draw-map-water)" }); if (on.building && data.building) tail.push({ label: `건물 ${data.building.features.length.toLocaleString()}동`, color: "var(--draw-map-building)", }); if (on.forestRoad && data.road) tail.push({ label: `임도 짐작 ${data.road.small.features.length}가닥`, 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; } /** 검증 창구 — 겹별 수(지물 · 넓이 m² · 길이 m). */ export const layerCounts = (data: Partial): Record => ({ ...(data.building && { building: data.building.features.length }), ...(data.water && { water: data.water.features.length }), ...(data.road && { road: data.road.all.features.length, forestRoad: data.road.small.features.length, }), ...(data.forestRoadFile && { forestRoadFile: data.forestRoadFile.info.count, forestRoadFile_m: data.forestRoadFile.info.length_m, forestRoadFile_total: data.forestRoadFile.info.total, }), ...(data.landslideZones && { landslideZone_points: data.landslideZones.info.points.length, landslideZone_parcels: data.landslideZones.parcels.features.length, landslideZone_missing: data.landslideZones.info.missing.length, }), ...(data.place && { place: data.place.length }), ...(data.zones && Object.fromEntries( Object.entries(data.zones.summary).flatMap(([kind, value]) => [ [`zone_${kind}`, value.count], [`zone_${kind}_m2`, value.area_m2], ]), )), ...(data.soils && Object.fromEntries( data.soils.summary.flatMap((soil) => [ [`soil_${soil.code}`, soil.count], [`soil_${soil.code}_m2`, soil.area_m2], ]), )), ...(data.bands && { band_stream_m2: data.bands.info.stream.area_m2, band_road_m2: data.bands.info.road.area_m2, }), ...(data.hillshade && { hillshade_px: data.hillshade.image.naturalWidth }), ...(data.landslide && { landslide_px: data.landslide.image.naturalWidth }), ...Object.fromEntries( LINE_KEYS.flatMap((key) => { const got = data[key]; return got ? [ [key, got.count], [`${key}_m`, got.length_m], ] : []; }), ), });