/* ============================================================================= * D03_PlanAnalysis_UI_LayerDefs.ts * D03 바탕 겹 받기 · 옛 캔버스 도움(PLAN 569-2 · 581-1 · 581-2 · 584-1 · 584-3 · 586-2 · 624-2) — 임도 짐작 구분 · 물 면 · * 면 겹 채우기 · 선 긋기 · 그림 겹 놓기 · GL 바탕 자료(지물 색 붙이기 · 그림 귀퉁이 · 음영 그늘 그림). * 겹 목록 · 처음 값 · 켬끔 취향 · 선 겹 꼴 · 보호구역 색 = 공용 `A00_Common/map/map_layer_list`(624-2). * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 그리기 차례 · 범례). * ========================================================================== */ import type { MapLayerKey } from "../A00_Common/map/map_layer_list"; import { drawPreparedLayer, normalizedToScreen, type GeoJsonCollection, 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"; import type { GlImage } from "./D03_PlanAnalysis_UI_MapGl"; /** 겹 목록 · 페이지 표 = 공용 `map_layer_list`(624-2) — 옛 이름으로 다시 내놓음(캔버스 · 조각 · 임도망 목록이 읽음). */ export type PlanLayerKey = MapLayerKey; export { LINE_KEYS, LINE_LAYERS, ZONE_COLORS, type RoadPick, } from "../A00_Common/map/map_layer_list"; /** 도로 중 기존 임도로 짐작하는 구분. */ 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 { // 세 귀퉁이로 놓음 — TM 격자는 경위도 바탕에 대해 수렴각만큼 돌아 있음(586-0) const [x0, y0, x1, y1] = raster.raster.box_m; const [left, top] = map.toScreen([x0, y1]); const [right, rightY] = map.toScreen([x1, y1]); const [bottomX, bottom] = map.toScreen([x0, y0]); const { naturalWidth: w, naturalHeight: h } = raster.image; context.transform( (right - left) / w, (rightY - top) / w, (bottomX - left) / h, (bottom - top) / h, left, top, ); context.drawImage(raster.image, 0, 0); } /** GL 자료 — 지물마다 그릴 색(`_color`) · 덧 속성을 붙인 사본. */ export function glTagged( collection: GeoJsonCollection, color: (props: Record) => string, extra: Record = {}, ): GeoJsonCollection { return { ...collection, features: (collection.features ?? []).map((feature) => ({ ...feature, properties: { ...feature.properties, ...extra, _color: color(feature.properties ?? {}) }, })), }; } /** GL 그림 자리 — 서버 경위도 귀퉁이(왼쪽 아래부터 돎) → 왼쪽 위부터 · 없으면 null. */ export function glImage(raster: PlanRaster, url = raster.image): GlImage | null { const box = raster.box; return box?.length === 4 ? { url, corners: [box[3], box[2], box[1], box[0]] } : null; } /** 음영 그늘 그림 — 위성 위 「곱하기 0.75」와 같은 검은 반투명(밝기 g → 투명도 0.75 · (1 − g)). */ export function shadeImage(image: HTMLImageElement): string { const canvas = document.createElement("canvas"); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; const context = canvas.getContext("2d")!; context.drawImage(image, 0, 0); const pixels = context.getImageData(0, 0, canvas.width, canvas.height); const px = pixels.data; for (let i = 0; i < px.length; i += 4) { px[i + 3] = Math.round(0.75 * (255 - px[i]) * (px[i + 3] / 255)); px[i] = px[i + 1] = px[i + 2] = 0; } context.putImageData(pixels, 0, 0); return canvas.toDataURL("image/png"); }