- 계산 묶음 drainage 단계(pipelineStage · 붙이기 = 묶음 쪽) — 노선 초안 · 관이 바뀌면 낡음 · 자동 = 3 s 조용한 뒤 「지금 노선으로 셈」 · 수동 = 칸만 흐림 + 가운데 [업데이트] · 구석 「계산 중」 · 다시 조작하면 요청 끊고 처음으로 · 덮개 없음 - ⚙ 설정 창(배수유역 칸 머리 · openModal) 수동(기본) / 자동 · 계정 화면 설정 drainage-mode(b_page_state 등록표 한 줄 · build:corridor 다시 지음) - 늦은 답 버림 = 차례 번호 · 서버 superseded · 세부유역 요청 감싸기와 같은 차례(B05_Profile_UI_Drainage_Run 으로 나눔) - 제안 = 관경(Ø→Ø) · 시설(BOX암거 · 세월교) · 관 자리 후보(지도 점선 원) · [적용] 한 번에 관 초안 · 자동 바꿈 없음 - [확정] savePipes(confirm) = PUT ?recompute=true · [저장] = 낡음 달고 저장 · 패널 691줄 - 순수 규칙 logic/B05_Profile_Drainage_Live · _Suggest · 시험 test_347_2_drainage_live_ui · typecheck 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
228 lines
9.0 KiB
TypeScript
228 lines
9.0 KiB
TypeScript
/* =============================================================================
|
|
* 배수유역도 캔버스 렌더러 (B05)
|
|
*
|
|
* 한 프레임에 무엇을 어떤 순서로 그릴지만 담는다. 상태는 갖지 않고 패널이 매 프레임 넘긴
|
|
* 묶음(`DrainageScene`)만 본다 — 패널 본체(`_UI_Drainage_Panel.ts`)가 700줄 한계에 닿아
|
|
* 분리했다.
|
|
*
|
|
* 쌓는 순서(아래 → 위)
|
|
* 세부유역 채움 · 전체 유역 외곽선 → 도엽 레이어 → 상류 세류 → 계획선 →
|
|
* 유입 강도 색칠 → 평균 흐름 화살표 → 유입 집중점 → 배관 마커
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
drawPreparedLayer,
|
|
routeLineColor,
|
|
ROUTE_LINE_WIDTH,
|
|
type Normalizer,
|
|
type PreparedLayer,
|
|
type ViewState,
|
|
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
|
import {
|
|
drawFilledRing,
|
|
drawRidgeRing,
|
|
drawRingBadge,
|
|
drawStationTicks,
|
|
drawUpstreamLines,
|
|
ringCenterOnScreen,
|
|
} from "../B04_PreProcess/B04_PreProcess_UI_MapOverlays";
|
|
import type { DetailBasin, VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import { drawFlowArrows, type FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows";
|
|
import { drawStrengthLine } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
|
|
import type { RoutePoint } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
|
|
import {
|
|
basinColor,
|
|
createMetricProjector,
|
|
drawHotspots,
|
|
DRAINAGE_LAYERS,
|
|
layerColor,
|
|
type DrainageLayer,
|
|
} from "./B05_Profile_UI_Drainage_Parts";
|
|
import type { PipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
|
|
import { drawRouteSpans, type RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans";
|
|
|
|
export interface DrainageScene {
|
|
meta: VWorldMeta | null;
|
|
normalizer: Normalizer | null;
|
|
basins: ReadonlyArray<DetailBasin>;
|
|
selectedBasin: number | null;
|
|
mainBoundary: ReadonlyArray<[number, number]>;
|
|
preparedLayers: ReadonlyMap<DrainageLayer, PreparedLayer>;
|
|
activeLayers: ReadonlySet<DrainageLayer>;
|
|
routeLayer: PreparedLayer | null;
|
|
upstreamLines: ReadonlyArray<Array<[number, number]>>;
|
|
showUpstream: boolean;
|
|
strengthSamples: ReadonlyArray<RoutePoint>;
|
|
strength: Float64Array;
|
|
maxStrength: number;
|
|
showStrength: boolean;
|
|
flowArrows: ReadonlyArray<FlowArrow>;
|
|
arrowSpacingM: number;
|
|
showArrows: boolean;
|
|
hotspots: ReadonlyArray<{ chainage: number; area: number }>;
|
|
maxHotspotArea: number;
|
|
showHotspots: boolean;
|
|
pipeEditor: PipeEditor;
|
|
pipeColor: (chainage: number, position: number) => string;
|
|
/** 선택된 측점의 누가거리(m). 계획선 위 그 자리에 선택 표식을 그린다(null=없음). */
|
|
markedChainage: number | null;
|
|
/** 규칙 측점 간격(m) — 눈금·번호 표기 기준 (2026-09-04 사용자 지시). */
|
|
stationIntervalM: number;
|
|
/** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). 없으면 빈 배열. */
|
|
intervalSpans: ReadonlyArray<RouteSpanBand>;
|
|
/** 관 자리 후보 누가거리(347-3 · 제안만) — 계획선 위 점선 원. */
|
|
candidates: ReadonlyArray<number>;
|
|
}
|
|
|
|
export function drawDrainageScene(
|
|
context: CanvasRenderingContext2D,
|
|
view: ViewState,
|
|
scene: DrainageScene,
|
|
): void {
|
|
const { meta, normalizer } = scene;
|
|
// 유역 번호는 **맨 위에** 얹는다 — 채움과 함께 그리면 등고선·화살표·관 마커에 가려진다
|
|
// (2026-08-01 사용자 지시). 자리는 채움을 그리면서 같이 모아 둔다.
|
|
const badges: Array<{ center: [number, number]; color: string; label: string }> = [];
|
|
// 세부유역 채움을 가장 아래에 깔아 등고선·세류 판독을 가리지 않게 한다.
|
|
if (normalizer) {
|
|
scene.basins.forEach((basin) => {
|
|
const color = basinColor(basin.index);
|
|
if (basin.polygon_lonlat.length >= 3) {
|
|
badges.push({
|
|
center: ringCenterOnScreen(basin.polygon_lonlat, normalizer, view),
|
|
color,
|
|
label: String(basin.index),
|
|
});
|
|
}
|
|
drawFilledRing(
|
|
context,
|
|
{ ring: basin.polygon_lonlat, rings: basin.polygon_rings_lonlat },
|
|
normalizer,
|
|
view,
|
|
// 하나를 고르면 나머지는 옅게 물러난다.
|
|
scene.selectedBasin === null || scene.selectedBasin === basin.index
|
|
? color
|
|
: color.replace(/0\.45\)$/, "0.18)"),
|
|
);
|
|
});
|
|
// 전체 유역 외곽선 = 분수령(능선). 해석 결과를 그대로 그린다 — 손으로 고치지 않는다.
|
|
if (scene.mainBoundary.length > 2) {
|
|
drawRidgeRing(context, scene.mainBoundary as Array<[number, number]>, normalizer, view);
|
|
}
|
|
}
|
|
// 등고선을 얇게 깔고 세류를 그 위에, 노선을 맨 위에 둔다.
|
|
DRAINAGE_LAYERS.forEach((layer) => {
|
|
if (!scene.activeLayers.has(layer)) return;
|
|
const prepared = scene.preparedLayers.get(layer);
|
|
if (!prepared) return;
|
|
context.lineWidth = layer === "도엽_등고선" ? 0.7 : 1.5;
|
|
context.strokeStyle = layerColor(layer);
|
|
drawPreparedLayer(context, prepared, view, "dot");
|
|
});
|
|
// 상류 세류선 강조 — 유역 채움 위, 흐름 화살표 아래. B04와 같은 공용 렌더러.
|
|
if (scene.showUpstream && normalizer && scene.upstreamLines.length > 0) {
|
|
drawUpstreamLines(
|
|
context,
|
|
scene.upstreamLines as Array<Array<[number, number]>>,
|
|
normalizer,
|
|
view,
|
|
);
|
|
}
|
|
if (scene.routeLayer) {
|
|
context.lineWidth = ROUTE_LINE_WIDTH;
|
|
context.strokeStyle = routeLineColor();
|
|
drawPreparedLayer(context, scene.routeLayer, view, "dot");
|
|
}
|
|
if (!meta) {
|
|
scene.pipeEditor.draw(context, view, scene.pipeColor);
|
|
drawBadges(context, badges);
|
|
return;
|
|
}
|
|
const projector = createMetricProjector(meta, view);
|
|
// 구간형 구조물 띠 — 계획선 바로 위, 강도 색칠 아래. 종단 레인의 띠와 같은 뜻이다.
|
|
if (scene.intervalSpans.length > 0) {
|
|
drawRouteSpans(context, scene.strengthSamples, scene.intervalSpans, projector.toScreen);
|
|
}
|
|
// 유입 강도 색칠 — 계획선 위, 배관 마커 아래. 색띠는 B04 지도와 공용이다.
|
|
if (scene.showStrength && scene.strength.length > 0) {
|
|
drawStrengthLine(context, scene.strengthSamples, scene.strength, scene.maxStrength, (point) =>
|
|
projector.toScreen(point.x, point.y),
|
|
);
|
|
}
|
|
// 평균 흐름 화살표 — 좌표는 사업지 CRS(m)라 도엽 메타로 바로 화면에 옮긴다.
|
|
if (scene.showArrows && scene.flowArrows.length > 0) {
|
|
drawFlowArrows(
|
|
context,
|
|
scene.flowArrows as FlowArrow[],
|
|
scene.arrowSpacingM,
|
|
projector.pxPerMeter,
|
|
projector.toScreen,
|
|
view,
|
|
);
|
|
}
|
|
// 유입 집중점 마커 — 강도 색칠 위, 배관 마커 아래.
|
|
if (scene.showHotspots && scene.hotspots.length > 0) {
|
|
drawHotspots(
|
|
context,
|
|
projector.toScreen,
|
|
scene.strengthSamples,
|
|
scene.hotspots,
|
|
scene.maxHotspotArea,
|
|
);
|
|
}
|
|
// 관 자리 후보 — 점선 원(배관 마커 아래 · 제안만이라 채우지 않음).
|
|
scene.candidates.forEach((chainage) => {
|
|
const sample = scene.strengthSamples[Math.round(chainage)];
|
|
if (!sample) return;
|
|
const [x, y] = projector.toScreen(sample.x, sample.y);
|
|
context.save();
|
|
context.setLineDash([3, 3]);
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = "#1e88e5";
|
|
context.beginPath();
|
|
context.arc(x, y, 9, 0, Math.PI * 2);
|
|
context.stroke();
|
|
context.restore();
|
|
});
|
|
// 배관(관 매설) 마커 — 계획선 위.
|
|
scene.pipeEditor.draw(context, view, scene.pipeColor);
|
|
// 측점 선택 마킹 — 계획선 위 다이아몬드 표식(2026-08-05 사용자 지시. 3자 선택 동기화의
|
|
// 배수유역도 쪽 표현. 유역이 없는 구조물 측점도 위치가 보여야 한다).
|
|
if (scene.markedChainage !== null && scene.strengthSamples.length > 0) {
|
|
const index = Math.min(
|
|
scene.strengthSamples.length - 1,
|
|
Math.max(0, Math.round(scene.markedChainage)),
|
|
);
|
|
const sample = scene.strengthSamples[index];
|
|
const [x, y] = projector.toScreen(sample.x, sample.y);
|
|
context.save();
|
|
context.translate(x, y);
|
|
context.rotate(Math.PI / 4);
|
|
const half = 7;
|
|
context.beginPath();
|
|
context.rect(-half, -half, half * 2, half * 2);
|
|
context.fillStyle = "rgba(255, 196, 0, 0.35)";
|
|
context.fill();
|
|
context.lineWidth = 2.5;
|
|
context.strokeStyle = "#ff9800";
|
|
context.stroke();
|
|
context.restore();
|
|
}
|
|
// 측점 눈금·번호 — 관 마커 위, 유역 번호 아래 (2026-09-04 사용자 지시). B04 지도와 공용.
|
|
drawStationTicks(context, scene.strengthSamples, {
|
|
intervalM: scene.stationIntervalM,
|
|
pxPerMeter: projector.pxPerMeter,
|
|
toScreen: projector.toScreen,
|
|
avoidChainages: scene.pipeEditor.chainages(),
|
|
});
|
|
// 유역 번호 — 무엇에도 가리지 않게 맨 마지막.
|
|
drawBadges(context, badges);
|
|
}
|
|
|
|
function drawBadges(
|
|
context: CanvasRenderingContext2D,
|
|
badges: ReadonlyArray<{ center: [number, number]; color: string; label: string }>,
|
|
): void {
|
|
badges.forEach(({ center, color, label }) => drawRingBadge(context, center, color, label));
|
|
}
|