B04 - 세부유역을 눌러 고르면 그 유역만 진하게, 나머지는 옅게 물러난다. 관 마커를 눌러도 그 관이 받는 유역이 골라지고, 유역 밖을 누르면 강조가 풀린다. - 고른 유역의 제원(측점·면적·표고차·유하장)을 상태 줄에 표기한다. B05 - "배관 편집" 토글을 없애고 마커를 언제나 끌 수 있게 했다. 계획선을 그냥 누르는 것으로는 배관이 생기지 않는다 — 유역을 고르려 할 때마다 배관이 생기기 때문이다. 추가·삭제는 B04와 같은 우클릭 메뉴로 옮겼다. - 배관을 옮기거나 넣거나 지우면 세부유역을 즉시 다시 나눈다(격자 해석 없음). - "자동 제안" 버튼 이름을 "초기화"로 바꿨다. - 노선 유입 강도 색칠을 B04 지도와 같은 색띠로 추가했다(토글 포함). 공용화 - common_util_drainage_detail: 1m 구간별 유입 면적 곡선을 응답에 포함(B04·B05 동일). - B04_wf1_Surface_UI_FlowRamp.ts: 색띠·정규화·강도선 렌더러를 두 화면이 공유. - ui_template_context_menu.ts: 지도 우클릭 메뉴를 공용 부품으로 분리. - B05_wf2_Route_UI_Drainage_Parts.ts 신설로 패널을 700줄 규칙 안으로 되돌림(677줄). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
4.4 KiB
TypeScript
103 lines
4.4 KiB
TypeScript
/* =============================================================================
|
|
* 도로 유입 강도 색띠 (B04 2D 지도 · B05 배수유역도 공용)
|
|
*
|
|
* 계획선 **1m 구간마다** 그 자리로 모이는 상류 면적을 색으로 칠한다. 두 화면이 같은 값을
|
|
* 같은 색으로 보여야 대조가 되므로 색띠와 정규화 규칙을 여기 한 곳에 둔다.
|
|
*
|
|
* 색 단계는 로그 스케일이다. 계곡 한 지점이 사면보다 수백 배 크기 때문에 선형으로 칠하면
|
|
* 몇 점만 빨갛고 나머지는 전부 파랑으로 뭉친다(2026-08-01 사용자 지시).
|
|
* ========================================================================== */
|
|
|
|
import { themeColor } from "@ui/ui_template_palette";
|
|
import type { RoutePoint } from "./B04_wf1_Surface_UI_RouteSamples";
|
|
|
|
/** 강도 색띠 — 파랑(적음) → 빨강(많음). 정의처는 `ui_template_theme.css`. */
|
|
const RAMP_TOKENS: ReadonlyArray<[name: string, fallback: string]> = [
|
|
["--map-flow-ramp-0", "#2563eb"],
|
|
["--map-flow-ramp-1", "#06b6d4"],
|
|
["--map-flow-ramp-2", "#22c55e"],
|
|
["--map-flow-ramp-3", "#eab308"],
|
|
["--map-flow-ramp-4", "#f97316"],
|
|
["--map-flow-ramp-5", "#dc2626"],
|
|
];
|
|
|
|
/** 강도 선 굵기(px). 계획선(2.4)보다 굵어야 그 위에 얹힌 것으로 읽힌다. */
|
|
export const STRENGTH_LINE_WIDTH = 5;
|
|
|
|
/** `#rrggbb` → [r,g,b]. 색띠는 hex로만 정의한다(보간을 위해). */
|
|
function parseHex(color: string): [number, number, number] {
|
|
const hex = color.trim().replace("#", "");
|
|
const full =
|
|
hex.length === 3
|
|
? hex
|
|
.split("")
|
|
.map((c) => c + c)
|
|
.join("")
|
|
: hex;
|
|
const value = Number.parseInt(full.slice(0, 6), 16);
|
|
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
|
}
|
|
|
|
/** 0~1을 색띠 위에서 보간한다. */
|
|
export function rampColor(t: number): string {
|
|
const clamped = Math.min(1, Math.max(0, t));
|
|
const last = RAMP_TOKENS.length - 1;
|
|
const position = clamped * last;
|
|
const low = Math.min(last, Math.floor(position));
|
|
const high = Math.min(last, low + 1);
|
|
const ratio = position - low;
|
|
const a = parseHex(themeColor(RAMP_TOKENS[low][0], RAMP_TOKENS[low][1]));
|
|
const b = parseHex(themeColor(RAMP_TOKENS[high][0], RAMP_TOKENS[high][1]));
|
|
const mix = (index: number): number => Math.round(a[index] + (b[index] - a[index]) * ratio);
|
|
return `rgb(${mix(0)}, ${mix(1)}, ${mix(2)})`;
|
|
}
|
|
|
|
/** 색띠 위치를 정하는 정규화(로그 스케일). 최대가 0이면 전부 0. */
|
|
export function normalizeStrength(value: number, maximum: number): number {
|
|
if (maximum <= 0 || value <= 0) return 0;
|
|
return Math.log1p(value) / Math.log1p(maximum);
|
|
}
|
|
|
|
/** 백엔드가 준 [누가거리, 면적] 목록을 인덱스=누가거리(m) 배열로 편다. */
|
|
export function buildStrengthArray(profile: ReadonlyArray<readonly [number, number]>): {
|
|
strength: Float64Array<ArrayBuffer>;
|
|
maximum: number;
|
|
} {
|
|
const length = profile.reduce((max, [chainage]) => Math.max(max, chainage), 0);
|
|
const strength = new Float64Array(Math.floor(length) + 1);
|
|
profile.forEach(([chainage, area]) => {
|
|
const index = Math.round(chainage);
|
|
if (index >= 0 && index < strength.length) strength[index] = area;
|
|
});
|
|
// 노선이 길면 점이 수만 개가 되므로 spread(Math.max(...))로 최대를 구하지 않는다.
|
|
const maximum = strength.reduce((max, value) => (value > max ? value : max), 0);
|
|
return { strength, maximum };
|
|
}
|
|
|
|
/** 1m 재표본 계획선 위에 강도 색을 칠한다. 화면 변환은 호출부가 넘긴다. */
|
|
export function drawStrengthLine(
|
|
context: CanvasRenderingContext2D,
|
|
samples: ReadonlyArray<RoutePoint>,
|
|
strength: Float64Array,
|
|
maximum: number,
|
|
toScreen: (point: RoutePoint) => [number, number],
|
|
): void {
|
|
if (samples.length < 2 || strength.length === 0) return;
|
|
context.save();
|
|
context.lineWidth = STRENGTH_LINE_WIDTH;
|
|
context.lineCap = "round";
|
|
const limit = Math.min(strength.length, samples.length - 1);
|
|
for (let index = 0; index < limit; index += 1) {
|
|
const value = strength[index];
|
|
if (value <= 0) continue; // 유입 없는 구간은 계획선 원래 색을 그대로 둔다
|
|
const [x0, y0] = toScreen(samples[index]);
|
|
const [x1, y1] = toScreen(samples[index + 1]);
|
|
context.strokeStyle = rampColor(normalizeStrength(value, maximum));
|
|
context.beginPath();
|
|
context.moveTo(x0, y0);
|
|
context.lineTo(x1, y1);
|
|
context.stroke();
|
|
}
|
|
context.restore();
|
|
}
|