/* ============================================================================= * 평균 흐름 화살표 렌더러 (B04·B05 공용) * * 백엔드가 10m 블록 평균으로 뽑아 둔 화살표를 그린다. 셀 화살표(1m)는 도면 배율에서 * 경향이 안 보이므로 B05는 이 화살표만 쓴다. * * 화살표 길이는 **간격보다 짧게** 잡아 서로 닿지 않게 한다 — 격자처럼 맞물리면 * 방향이 아니라 그물망으로 읽힌다. * * 좌표계는 페이지마다 다르다(B04는 lon/lat 정규화, B05는 사업지 CRS 미터). 그래서 * 화면 변환은 호출부가 `project`로 넘긴다 — 이 파일은 좌표계를 모른다. * ========================================================================== */ import { themeColor } from "@ui/ui_template_palette"; import { haloColor } from "./B04_PreProcess_UI_MapRender"; /** 화살표 1개 — [가로, 세로, 방위(도), 도로 도달, 셀 수]. 앞 두 값의 좌표계는 호출부가 정한다. */ export type FlowArrow = [number, number, number, boolean, number]; /** 화살표 길이를 간격의 몇 배로 할지. 1보다 작아야 서로 닿지 않는다. */ const LENGTH_RATIO = 0.55; /** 선 두께를 길이의 몇 배로 할지. */ const WIDTH_RATIO = 0.07; /** 이보다 짧으면 방향이 안 읽히므로 그리지 않는다(px). */ const MIN_LENGTH_PX = 9; /** 화면을 가득 채우지 않도록 두는 상한(px). */ const MAX_LENGTH_PX = 40; /* 색 값의 정의처는 `ui_template_theme.css`(`--map-*`)다 — 여기서 값을 새로 정하지 않는다. */ const toRoadColor = (): string => themeColor("--map-flow-to-road-line", "rgba(153, 27, 27, 0.95)"); const awayColor = (): string => themeColor("--map-flow-away-arrow", "rgba(30, 64, 175, 0.95)"); /** 화살표 좌표를 캔버스 픽셀로 옮기는 함수. */ export type ArrowProjector = (a: number, b: number) => readonly [number, number]; /** * 평균 흐름 화살표를 그린다. * * `spacingM`은 화살표 사이 실제 간격(m), `pxPerMeter`는 현재 배율에서 1m가 몇 px인지. * 둘을 곱해 길이를 정하므로 확대·축소에 따라 화살표도 같이 커지고 작아진다. */ export function drawFlowArrows( context: CanvasRenderingContext2D, arrows: ReadonlyArray, spacingM: number, pxPerMeter: number, project: ArrowProjector, canvas: { readonly width: number; readonly height: number }, ): void { if (arrows.length === 0 || spacingM <= 0 || pxPerMeter <= 0) return; const length = Math.min(spacingM * pxPerMeter * LENGTH_RATIO, MAX_LENGTH_PX); if (length < MIN_LENGTH_PX) return; const reach = length / 2; const head = length * 0.26; const width = Math.max(0.8, length * WIDTH_RATIO); context.save(); context.lineCap = "round"; context.lineJoin = "round"; context.setLineDash([]); arrows.forEach(([a, b, degrees, reaches]) => { const [x, y] = project(a, b); if (x < -length || x > canvas.width + length) return; if (y < -length || y > canvas.height + length) return; const angle = (degrees * Math.PI) / 180; const unitX = Math.cos(angle); const unitY = Math.sin(angle); const tailX = x - unitX * reach; const tailY = y - unitY * reach; const tipX = x + unitX * reach; const tipY = y + unitY * reach; // 어두운 배경·채움색 위에서도 읽히도록 흰 테두리를 한 겹 깔고 그 위에 색을 얹는다. for (const [color, lineWidth] of [ [haloColor(), width + 1.4] as const, [reaches ? toRoadColor() : awayColor(), width] as const, ]) { context.strokeStyle = color; context.lineWidth = lineWidth; context.beginPath(); context.moveTo(tailX, tailY); context.lineTo(tipX, tipY); context.moveTo(tipX, tipY); context.lineTo(tipX - (unitX + unitY * 0.65) * head, tipY - (unitY - unitX * 0.65) * head); context.moveTo(tipX, tipY); context.lineTo(tipX - (unitX - unitY * 0.65) * head, tipY - (unitY + unitX * 0.65) * head); context.stroke(); } }); context.restore(); }