/* ============================================================================= * 2D 지도 오버레이 도형 (B04 지도 · B05 배수유역도 공용) * * 유역 채움·번호 배지·분수령 파선·상류 세류망처럼 **배경 레이어 위에 얹는** 도형만 모았다. * 좌표 변환과 레이어 렌더는 `B04_PreProcess_UI_MapRender.ts`가 맡는다 — 그 파일이 700줄 * 한계에 닿아 분리했다. * ========================================================================== */ import { themeColor } from "@ui/ui_template_palette"; /** 상류 세류망 강조선 색. 정의처는 `ui_template_theme.css`(`--map-upstream`). */ const upstreamLineColor = (): string => themeColor("--map-upstream", "rgba(29, 78, 216, 0.95)"); import { haloColor, lonLatToScreen, type Normalizer, type ViewState, } from "./B04_PreProcess_UI_MapRender"; export type FilledRing = { ring: ReadonlyArray; /** 면적 중심에 얹을 번호. 없으면 라벨을 그리지 않는다. */ label?: string; }; /** * lon/lat 폴리곤 링을 파스텔 채움 + 테두리 + 중심 번호로 그린다. * 사전 투영 캐시를 쓰지 않는 소량(유역 수 개) 오버레이 전용이라 매 프레임 변환해도 부담이 없다. */ export function drawFilledRing( context: CanvasRenderingContext2D, entry: FilledRing, normalizer: Normalizer, view: ViewState, color: string, ): void { if (entry.ring.length < 3) return; let sumX = 0; let sumY = 0; context.beginPath(); entry.ring.forEach(([lon, lat], index) => { const [x, y] = lonLatToScreen(normalizer, view, lon, lat); sumX += x; sumY += y; if (index === 0) context.moveTo(x, y); else context.lineTo(x, y); }); context.closePath(); context.fillStyle = color; context.fill(); context.strokeStyle = color; context.lineWidth = 1.6; context.stroke(); if (!entry.label) return; drawRingBadge(context, [sumX / entry.ring.length, sumY / entry.ring.length], color, entry.label); } /** 유역 번호 배지 한 개(라운드 사각 + 숫자). 화면 좌표를 그대로 받는다. * * 채움과 따로 떼어 둔 이유: 번호는 **맨 위에** 있어야 한다. 채움과 함께 그리면 그 위에 * 얹히는 등고선·화살표·관 마커에 가려 읽을 수 없다(2026-08-01 사용자 지시). * * 원이 아니라 사각인 이유: 지도 위 다른 숫자들도 전부 동그라미라 크기로만 갈렸다. * 형태를 바꿔 한눈에 구분되게 했다 — 크기는 그대로다(2026-08-17 사용자 지시). */ export function drawRingBadge( context: CanvasRenderingContext2D, center: readonly [number, number], color: string, label: string, ): void { const [centerX, centerY] = center; const half = 11; context.save(); context.beginPath(); context.roundRect(centerX - half, centerY - half, half * 2, half * 2, 6); context.fillStyle = color; context.fill(); context.strokeStyle = haloColor(); context.lineWidth = 1.5; context.stroke(); context.font = "600 12px sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; context.fillStyle = themeColor("--map-label-text", "#1f2937"); context.fillText(label, centerX, centerY); context.restore(); } /** 폴리곤 정점 평균의 화면 좌표 — 배지를 얹을 자리. */ export function ringCenterOnScreen( ring: ReadonlyArray, normalizer: Normalizer, view: ViewState, ): [number, number] { let sumX = 0; let sumY = 0; ring.forEach(([lon, lat]) => { const [x, y] = lonLatToScreen(normalizer, view, lon, lat); sumX += x; sumY += y; }); return [sumX / ring.length, sumY / ring.length]; } /** 상류 세류망 강조 — B04 분석 오버레이와 B05 배수유역도가 같은 굵기·색으로 그린다. */ export function drawUpstreamLines( context: CanvasRenderingContext2D, lines: ReadonlyArray>, normalizer: Normalizer, view: ViewState, ): void { if (lines.length === 0) return; context.save(); context.setLineDash([]); context.lineWidth = 4; context.lineCap = "round"; context.lineJoin = "round"; context.strokeStyle = upstreamLineColor(); lines.forEach((line) => { if (line.length < 2) return; context.beginPath(); line.forEach(([lon, lat], index) => { const [x, y] = lonLatToScreen(normalizer, view, lon, lat); if (index === 0) context.moveTo(x, y); else context.lineTo(x, y); }); context.stroke(); }); context.restore(); } /** 유역 경계(분수령=능선)를 능선 스타일(갈색 파선)로 강조해 그린다. */ export function drawRidgeRing( context: CanvasRenderingContext2D, ring: ReadonlyArray, normalizer: Normalizer, view: ViewState, ): void { if (ring.length < 3) return; context.beginPath(); ring.forEach(([lon, lat], index) => { const [x, y] = lonLatToScreen(normalizer, view, lon, lat); if (index === 0) context.moveTo(x, y); else context.lineTo(x, y); }); context.closePath(); context.save(); context.strokeStyle = themeColor("--map-basin-outline", "#92400e"); context.lineWidth = 1.8; context.setLineDash([7, 4]); context.stroke(); context.restore(); }