/* ============================================================================= * D03_PlanAnalysis_UI_LayerDefs.ts * D03 바탕 겹 정의(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3) — 겹 단추 묶음 · 처음 값 · 보호구역 · 토양형 색 · * 산사태위험지도 범례 · 선 겹(단층 · 지질구조선 · 0차곡) 꼴 · 켬끔 취향 읽기 · 면 겹 채우기 · 선 긋기 · 그림 겹 놓기. * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례). * ========================================================================== */ import type { IconName } from "@ui/ui_template_icons"; import type { LegendItem } from "@ui/ui_template_flow_legend"; import { readByKey } from "../A00_Common/b_page_state"; import { drawPreparedLayer, normalizedToScreen, type PreparedLayer, type ViewState, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import type { PlanRaster } from "./D03_PlanAnalysis_Api"; import type { ScreenMap } from "./D03_PlanAnalysis_UI_Map"; export type PlanLayerKey = | "satellite" | "hillshade" | "contour" | "slope" | "stream" | "water" | "road" | "forestRoad" | "forestRoadFile" | "cadastral" | "zones" | "soils" | "landslide" | "landslideZones" | "faults" | "lineaments" | "zeroOrder" | "bands" | "building" | "place"; export interface LayerDef { key: PlanLayerKey; icon: IconName; title: string; on: boolean; } /** 단추 묶음(사이 나눔선) — 바탕 · 물 · 길 · 구역. 「지금」 겹 처음 값 = 끔(`_분석_440-C` 1-4). */ export const GROUPS: readonly (readonly LayerDef[])[] = [ [ { key: "satellite", icon: "satellite", title: "위성사진", on: true }, { key: "hillshade", icon: "sun", title: "음영기복(2 m 지형 · 산줄기 · 골)", on: false }, { key: "contour", icon: "mountain", title: "도엽 등고선(굵은 선 = 계곡선 25 m)", on: true }, { key: "slope", icon: "angle", title: "급경사 색(15° · 35° · 40° 이상)", on: false }, ], [ { key: "stream", icon: "droplet", title: "세류선(가는 파랑) · 하천(굵은 파랑)", on: true }, { key: "water", icon: "ripple", title: "물 면(도엽 실폭하천 · 저수지 등)", on: false }, { key: "road", icon: "road", title: "기존 도로(도엽 도로중심선)", on: false }, { key: "forestRoad", icon: "route", title: "기존 임도 짐작(도엽 도로 소로 · 미분류 = 주황 점선 · 정식 임도망도 아님)", on: false, }, { key: "forestRoadFile", icon: "map", title: "올린 임도망도(산림청 시군구 SHP = 진한 주황 실선 · 옆 단추로 올림)", on: false, }, ], [ { key: "cadastral", icon: "borderAll", title: "지적도(참고용 · 법적 효력 없음)", on: false }, { key: "zones", icon: "shield", title: "보호 · 규제 구역(브이월드 여덟 종 · 참고용)", on: false, }, { key: "soils", icon: "leaf", title: "산림입지도(토양형 · 산림청 산림입지토양도 · 참고용)", on: false, }, { key: "landslide", icon: "alert", title: "산사태위험지도(1~5 등급 · 행안부 생활안전지도 · 참고용)", on: false, }, { key: "landslideZones", icon: "alertOctagon", title: "산사태취약지역(시도 지정 현황 · 점 + 지번 필지 테두리 · 참고용)", on: false, }, { key: "faults", icon: "bolt", title: "단층(브이월드 지질 · 참고용)", on: false }, { key: "lineaments", icon: "lineDashed", title: "지질구조선(브이월드 지질 · 참고용)", on: false, }, { key: "zeroOrder", icon: "arrowsJoin", title: "0차곡(지형 추정 골짜기 머리 · 위 집수 0.1~1 ha · 곡두 통과 피함)", on: false, }, { key: "bands", icon: "borderOuter", title: "계류 100 m · 도로 300 m 띠(타당성평가)", on: false, }, { key: "building", icon: "building", title: "건물(도엽 · 하류 민가 · 인가)", on: false }, { key: "place", icon: "mapPin", title: "지명(도엽 계곡 · 령 · 부락)", on: false }, ], ]; export const LAYERS: readonly LayerDef[] = GROUPS.flat(); /** 보호구역 종류 → 그림 색 값(서버 `ZONE_KINDS` 열쇠). */ export const ZONE_COLORS: Record = { forest: "--draw-map-zone-forest", baekdu: "--draw-map-zone-baekdu", water: "--draw-map-zone-water", park: "--draw-map-zone-park", heritage: "--draw-map-zone-heritage", wetland: "--draw-map-zone-wetland", wildlife: "--draw-map-zone-wildlife", greenbelt: "--draw-map-zone-greenbelt", }; /** 산사태위험지도 범례(1 등급 = 가장 위험). */ export const LANDSLIDE_LEGEND: LegendItem[] = [1, 2, 3, 4, 5].map((grade) => ({ label: `${grade}등급`, color: `var(--draw-map-landslide-${grade})`, })); /** 선 겹 — 이름 · 그림 꼴(서버 `LINE_KINDS` 열쇠 = `kind` · 없으면 0차곡). 새 선 겹 = 여기 한 줄. */ export type LineLayerKey = "faults" | "lineaments" | "zeroOrder"; export const LINE_LAYERS: Record< LineLayerKey, { label: string; kind?: string; color: string; width: number; dash: number[] } > = { faults: { label: "단층", kind: "faults", color: "--draw-map-fault", width: 2.4, dash: [] }, lineaments: { label: "지질구조선", kind: "lineaments", color: "--draw-map-lineament", width: 1.8, dash: [8, 4], }, zeroOrder: { label: "0차곡", color: "--draw-map-zero-order", width: 1.8, dash: [4, 3] }, }; export const LINE_KEYS = Object.keys(LINE_LAYERS) as LineLayerKey[]; /** 토양형 색 — 받은 범위 안 코드 차례로 돌려 씀. */ export const SOIL_COLORS = [1, 2, 3, 4, 5, 6].map((n) => `--draw-map-soil-${n}`); /** 도로 중 기존 임도로 짐작하는 구분. */ export const FOREST_ROAD_KINDS = new Set(["소로", "미분류"]); /** 물 면 = 도엽 수계(E) 면 셋. */ export const WATER_KEYS = ["실폭하천", "수계면E003", "수계면E005"]; export const hectare = (m2: number): string => `${(m2 / 10000).toFixed(1)} ha`; /** 면 겹 채움 — 지물마다 한 길(evenodd = 구멍) · 화면 밖 지물 거름(돌린 지도 귀퉁이까지 여유). */ export function fillLayer( context: CanvasRenderingContext2D, layer: PreparedLayer, view: ViewState, fill: string, alpha: number, edge?: { color: string; width: number; dash?: number[] }, ): void { const [ox, oy] = normalizedToScreen(view, 0, 0); const [ex, ey] = normalizedToScreen(view, 1, 1); const ax = ex - ox; const ay = ey - oy; const pad = Math.max(view.width, view.height) / 2; context.fillStyle = fill; if (edge) { context.strokeStyle = edge.color; context.lineWidth = edge.width; context.setLineDash(edge.dash ?? []); } for (const feature of layer.features) { if ( feature.maxX * ax + ox < -pad || feature.minX * ax + ox > view.width + pad || feature.maxY * ay + oy < -pad || feature.minY * ay + oy > view.height + pad ) continue; context.beginPath(); for (const { coords } of feature.parts) { context.moveTo(coords[0] * ax + ox, coords[1] * ay + oy); for (let i = 2; i < coords.length; i += 2) context.lineTo(coords[i] * ax + ox, coords[i + 1] * ay + oy); context.closePath(); } context.globalAlpha = alpha; context.fill("evenodd"); if (edge) { context.globalAlpha = 0.9; context.stroke(); } } context.setLineDash([]); } export const loadImage = (src: string): Promise => new Promise((done, fail) => { const image = new Image(); image.onload = () => done(image); image.onerror = () => fail(new Error("그림을 읽지 못했습니다")); image.src = src; }); /** 선 겹 긋기(점 솎음 `dot` · `everyM` = 등고 간격 거름). */ export function stroke( context: CanvasRenderingContext2D, map: ScreenMap, layer: PreparedLayer, color: string, width: number, everyM?: number, ): void { context.strokeStyle = color; context.lineWidth = width; drawPreparedLayer(context, layer, map.view, "dot", everyM); } /** 그림 겹(작업 좌표계 m 상자 · 위 = 북). */ export function drawRaster( context: CanvasRenderingContext2D, map: ScreenMap, raster: { raster: PlanRaster; image: HTMLImageElement }, ): void { const [x0, y0, x1, y1] = raster.raster.box_m; const [left, top] = map.toScreen([x0, y1]); const [right, bottom] = map.toScreen([x1, y0]); context.drawImage(raster.image, left, top, right - left, bottom - top); } /** 켬끔 취향(`key`) — 없거나 깨지면 처음 값. */ export function readPref(key: string): Record { const on = Object.fromEntries(LAYERS.map((def) => [def.key, def.on])) as Record< PlanLayerKey, boolean >; try { const saved = JSON.parse(readByKey(key) ?? "{}") as Partial>; for (const def of LAYERS) if (typeof saved[def.key] === "boolean") on[def.key] = saved[def.key] as boolean; } catch { // 깨진 값 = 처음 값 } return on; }