- ui_template_theme.css에 2D 지도 벡터 팔레트(--map-*) 33종을 유일한 정의처로 등록하고, 캔버스에서 CSS 변수를 읽는 공통 유틸 ui_template_palette.ts(themeColor)를 신설. 값은 한 번만 읽어 캐시하고 data-theme 변경 시 비운다(다크 전환 대응). - B04 지도·유역 오버레이·흐름 화살표와 B05 배수유역도·유역선 편집·배관 마커의 하드코딩 색상을 전부 토큰 조회로 교체. 계획선 색·굵기, 후광색은 공용 함수로 일원화. - B05 배수유역 패널의 사용자 문구를 전부 ui_locales로 이관(제목, 레이어 토글 5종, 도구 버튼 5종, 툴팁, 상태 문구 8종, 유역 제원 표기). B04 유역 분석 오버레이의 버튼·갈래 토글·진행/실패 안내도 함께 전환. 관리자 진단용 결과 판독문은 원문 유지. - A02 프로그램 소개의 6단계 제목과 A01 히어로 문구의 단계 나열을 진행단계 이름 (전처리/종단설계/횡단설계/상세설계/수량산출/설계도서)과 일치시킴. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
92 lines
4.0 KiB
TypeScript
92 lines
4.0 KiB
TypeScript
/* =============================================================================
|
|
* 평균 흐름 화살표 렌더러 (B04·B05 공용)
|
|
*
|
|
* 백엔드가 10m 블록 평균으로 뽑아 둔 화살표를 그린다. 셀 화살표(1m)는 도면 배율에서
|
|
* 경향이 안 보이므로 B05는 이 화살표만 쓴다.
|
|
*
|
|
* 화살표 길이는 **간격보다 짧게** 잡아 서로 닿지 않게 한다 — 격자처럼 맞물리면
|
|
* 방향이 아니라 그물망으로 읽힌다.
|
|
*
|
|
* 좌표계는 페이지마다 다르다(B04는 lon/lat 정규화, B05는 사업지 CRS 미터). 그래서
|
|
* 화면 변환은 호출부가 `project`로 넘긴다 — 이 파일은 좌표계를 모른다.
|
|
* ========================================================================== */
|
|
|
|
import { themeColor } from "@ui/ui_template_palette";
|
|
import { haloColor } from "./B04_wf1_Surface_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<FlowArrow>,
|
|
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();
|
|
}
|