feat(drainage): B05 평균 흐름 화살표 표기 + 응답 캐시 저장 누락 수정
This commit is contained in:
@@ -333,6 +333,8 @@ export interface WatershedAnalysis {
|
||||
/** B05용 평균 흐름 화살표 — [lon, lat, 방위(도), 도로도달, 셀 수].
|
||||
* 세류·도로 셀을 뺀 10m 블록 평균이라 사면 경향만 남는다. */
|
||||
flow_arrows: Array<[number, number, number, boolean, number]>;
|
||||
/** 화살표 사이 실제 간격(m). 화면이 화살표를 이보다 짧게 그려 서로 닿지 않게 한다. */
|
||||
arrow_spacing_m: number;
|
||||
/** 계산하지 않고 저장분을 그대로 돌려준 응답인지. */
|
||||
from_cache: boolean;
|
||||
/** 영구저장소에 남긴 검증용 GeoJSON 경로. */
|
||||
|
||||
@@ -43,7 +43,7 @@ from common_util.common_util_route_geometry import (
|
||||
)
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool
|
||||
from config.config_system import DRAINAGE_RESPONSE_FILENAME
|
||||
from config.config_system import DRAINAGE_ARROW_SPACING_M, DRAINAGE_RESPONSE_FILENAME
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B04 Surface Watershed"])
|
||||
@@ -298,6 +298,8 @@ async def get_primary_region(
|
||||
[*to_lonlat(x, y), round(math.degrees(angle), 1), reaches, cells]
|
||||
for x, y, angle, reaches, cells in preview.flow_arrows
|
||||
],
|
||||
# 화살표 간격(m). 화면이 화살표 크기를 정할 때 쓴다 — 서로 닿지 않게 이 값보다 짧게 그린다.
|
||||
"arrow_spacing_m": DRAINAGE_ARROW_SPACING_M,
|
||||
"from_cache": False,
|
||||
}
|
||||
# 단계 산출물을 영구저장소에 남긴다 — 기능을 붙일 때마다 여기에 단계가 하나씩 늘어난다.
|
||||
@@ -341,6 +343,8 @@ async def get_primary_region(
|
||||
)
|
||||
_write_stage_arrays(prepared["stored_path"], preview, domain, spec)
|
||||
_write_road_routing(prepared["stored_path"], preview, spec, prepared["route_line"], to_lonlat)
|
||||
# 응답 자체를 캐시로 남긴다 — 다음 조회는 배열을 재조립하지 않고 이 파일을 그대로 준다.
|
||||
_save_response(prepared["stored_path"], payload)
|
||||
return payload
|
||||
|
||||
|
||||
@@ -390,11 +394,28 @@ def _write_road_routing(
|
||||
)
|
||||
for pipe in preview.pipes
|
||||
],
|
||||
# 평균 흐름 화살표 — B05도 같은 그림을 그려야 하므로 여기 함께 남긴다.
|
||||
"flow_arrow": [
|
||||
(
|
||||
Point(x, y),
|
||||
{
|
||||
# B05 화면은 사업지 CRS(m)로 그리므로 미터 좌표도 함께 남긴다.
|
||||
"x": round(x, 2),
|
||||
"y": round(y, 2),
|
||||
"azimuth_deg": round(math.degrees(angle), 1),
|
||||
"reaches_road": reaches,
|
||||
"cells": cells,
|
||||
},
|
||||
)
|
||||
for x, y, angle, reaches, cells in preview.flow_arrows
|
||||
],
|
||||
},
|
||||
{
|
||||
"basin_area_m2": round(preview.basin_area_m2, 1),
|
||||
"pipe_count": len(preview.pipes),
|
||||
"route_length_m": round(route_line.length, 1),
|
||||
"arrow_count": len(preview.flow_arrows),
|
||||
"arrow_spacing_m": DRAINAGE_ARROW_SPACING_M,
|
||||
},
|
||||
to_lonlat,
|
||||
)
|
||||
@@ -495,11 +516,6 @@ def _boundary_geometry(ring: list[tuple[float, float]]) -> list[Any]:
|
||||
return [Polygon(ring)] if len(ring) >= 4 else []
|
||||
|
||||
|
||||
def _boundary_geometry(ring: list[tuple[float, float]]) -> list[Any]:
|
||||
"""2차 유역 외곽 링을 저장용 폴리곤으로 만든다(정점 3개 미만이면 비운다)."""
|
||||
return [Polygon(ring)] if len(ring) >= 4 else []
|
||||
|
||||
|
||||
def _as_polygons(geometry: Any) -> list[Any]:
|
||||
if geometry is None or geometry.is_empty:
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/* =============================================================================
|
||||
* 평균 흐름 화살표 렌더러 (B04·B05 공용)
|
||||
*
|
||||
* 백엔드가 10m 블록 평균으로 뽑아 둔 화살표를 그린다. 셀 화살표(1m)는 도면 배율에서
|
||||
* 경향이 안 보이므로 B05는 이 화살표만 쓴다.
|
||||
*
|
||||
* 화살표 길이는 **간격보다 짧게** 잡아 서로 닿지 않게 한다 — 격자처럼 맞물리면
|
||||
* 방향이 아니라 그물망으로 읽힌다.
|
||||
*
|
||||
* 좌표계는 페이지마다 다르다(B04는 lon/lat 정규화, B05는 사업지 CRS 미터). 그래서
|
||||
* 화면 변환은 호출부가 `project`로 넘긴다 — 이 파일은 좌표계를 모른다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 화살표 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;
|
||||
|
||||
const TO_ROAD_COLOR = "rgba(153, 27, 27, 0.95)";
|
||||
const AWAY_COLOR = "rgba(30, 64, 175, 0.95)";
|
||||
const HALO_COLOR = "rgba(255, 255, 255, 0.9)";
|
||||
|
||||
/** 화살표 좌표를 캔버스 픽셀로 옮기는 함수. */
|
||||
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 [
|
||||
[HALO_COLOR, width + 1.4] as const,
|
||||
[reaches ? TO_ROAD_COLOR : AWAY_COLOR, 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();
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchWatershedAnalysis, type WatershedAnalysis } from "./B04_wf1_Surface_Api_Fetch";
|
||||
import { drawFlowArrows } from "./B04_wf1_Surface_UI_FlowArrows";
|
||||
import type { Normalizer, ViewState } from "./B04_wf1_Surface_UI_MapRender";
|
||||
|
||||
/* =============================================================================
|
||||
@@ -32,11 +33,6 @@ const ARROW_SPACING_PX = 22;
|
||||
const BASIN_RING_COLOR = "rgba(146, 64, 14, 0.95)";
|
||||
/** 기본 관 마커. */
|
||||
const PIPE_COLOR = "rgba(249, 115, 22, 0.95)";
|
||||
/** 평균 흐름 화살표 — 10m 블록 평균. 셀 화살표보다 크게 그려 경향을 읽는다. */
|
||||
const MEAN_ARROW_TO_ROAD = "rgba(153, 27, 27, 0.95)";
|
||||
const MEAN_ARROW_AWAY = "rgba(30, 64, 175, 0.95)";
|
||||
const MEAN_ARROW_PX = 14;
|
||||
const MEAN_ARROW_MAX_PX = 46;
|
||||
|
||||
/** 개별로 켜고 끌 수 있는 오버레이 갈래. */
|
||||
const PARTS = [
|
||||
@@ -340,58 +336,33 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
/** B05에 얹을 평균 흐름 화살표 — 10m 블록 평균이라 축소해도 경향이 읽힌다. */
|
||||
function drawFlowArrows(
|
||||
/** B05에도 같이 쓰는 평균 흐름 화살표. 그리기는 공용 렌더러에 맡긴다. */
|
||||
function drawMeanArrows(
|
||||
context: CanvasRenderingContext2D,
|
||||
map: Normalizer,
|
||||
view: ViewState,
|
||||
region: WatershedAnalysis,
|
||||
): void {
|
||||
const arrows = region.flow_arrows ?? [];
|
||||
if (arrows.length === 0) return;
|
||||
// 격자 bbox의 경도 폭과 실폭(m)으로 1m당 픽셀을 환산한다.
|
||||
const lons = region.grid.bbox_lonlat.map(([lon]) => lon);
|
||||
const spanLon = Math.max(...lons) - Math.min(...lons);
|
||||
if (!(spanLon > 0) || !(region.grid.width_m > 0)) return;
|
||||
const ax = view.mapRect.width * view.scale;
|
||||
const pxPerMeter = ((spanLon / map.lonRange) * ax) / region.grid.width_m;
|
||||
const bx = view.mapRect.x * view.scale + (view.width / 2) * (1 - view.scale) + view.offsetX;
|
||||
const ay = view.mapRect.height * view.scale;
|
||||
const by = view.mapRect.y * view.scale + (view.height / 2) * (1 - view.scale) + view.offsetY;
|
||||
// 블록 간격(m)이 화면에서 몇 px인지로 화살표 크기를 정한다 — 확대하면 같이 커진다.
|
||||
const pxPerLon = ax / map.lonRange;
|
||||
const spacingPx =
|
||||
arrows.length > 1
|
||||
? Math.abs(arrows[1][0] - arrows[0][0]) * pxPerLon || MEAN_ARROW_PX
|
||||
: MEAN_ARROW_PX;
|
||||
const size = Math.max(MEAN_ARROW_PX, Math.min(spacingPx * 0.8, MEAN_ARROW_MAX_PX));
|
||||
|
||||
context.save();
|
||||
context.lineCap = "round";
|
||||
arrows.forEach(([lon, lat, degrees, reaches]) => {
|
||||
const x = ((lon - map.lonMin) / map.lonRange) * ax + bx;
|
||||
const y = (1 - (lat - map.latMin) / map.latRange) * ay + by;
|
||||
if (x < -size || x > view.width + size || y < -size || y > view.height + size) return;
|
||||
const angle = (degrees * Math.PI) / 180;
|
||||
const unitX = Math.cos(angle);
|
||||
const unitY = Math.sin(angle);
|
||||
const reach = size / 2;
|
||||
const tipX = x + unitX * reach;
|
||||
const tipY = y + unitY * reach;
|
||||
const head = size * 0.32;
|
||||
// 배경 대비를 위해 흰 테두리를 깔고 그 위에 색을 얹는다.
|
||||
for (const [color, lineWidth] of [
|
||||
["rgba(255, 255, 255, 0.9)", size * 0.18 + 2] as const,
|
||||
[reaches ? MEAN_ARROW_TO_ROAD : MEAN_ARROW_AWAY, size * 0.18] as const,
|
||||
]) {
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
context.beginPath();
|
||||
context.moveTo(x - unitX * reach, y - unitY * reach);
|
||||
context.lineTo(tipX, tipY);
|
||||
context.moveTo(tipX, tipY);
|
||||
context.lineTo(tipX - (unitX + unitY * 0.7) * head, tipY - (unitY - unitX * 0.7) * head);
|
||||
context.moveTo(tipX, tipY);
|
||||
context.lineTo(tipX - (unitX - unitY * 0.7) * head, tipY - (unitY + unitX * 0.7) * head);
|
||||
context.stroke();
|
||||
}
|
||||
});
|
||||
context.restore();
|
||||
drawFlowArrows(
|
||||
context,
|
||||
region.flow_arrows ?? [],
|
||||
region.arrow_spacing_m ?? 0,
|
||||
pxPerMeter,
|
||||
(lon, lat) => [
|
||||
((lon - map.lonMin) / map.lonRange) * ax + bx,
|
||||
(1 - (lat - map.latMin) / map.latRange) * ay + by,
|
||||
],
|
||||
view,
|
||||
);
|
||||
}
|
||||
|
||||
/** ⑦ 2차 전체 배수유역 외곽선(= 분수령)과 ⑧ 기본 관 위치. */
|
||||
@@ -549,7 +520,7 @@ export function createWatershedOverlay(onChange: () => void): WatershedOverlay {
|
||||
// 격자·화살표(유역 방향) → 1차 영역 → 2차 유역·관 순으로 아래에서 위로 쌓는다.
|
||||
if (shownParts.flow) drawGridCells(context, map, view, analysis);
|
||||
if (shownParts.primary) drawPrimaryRegion(context, map, view, analysis);
|
||||
if (shownParts.arrows) drawFlowArrows(context, map, view, analysis);
|
||||
if (shownParts.arrows) drawMeanArrows(context, map, view, analysis);
|
||||
if (shownParts.basin) drawBasinAndPipes(context, map, view, analysis);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -306,6 +306,10 @@ export interface DrainageBasinResponse {
|
||||
main_polygon_lonlat: Array<[number, number]>;
|
||||
/** B04 해석 격자 한 변(m). */
|
||||
grid_cell_m: number;
|
||||
/** 평균 흐름 화살표 — [x, y(사업지 CRS m), 방위(도), 도로도달, 셀 수]. */
|
||||
flow_arrows: Array<[number, number, number, boolean, number]>;
|
||||
/** 화살표 사이 실제 간격(m). */
|
||||
arrow_spacing_m: number;
|
||||
basins: DrainageBasin[];
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import json
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -58,6 +59,9 @@ class DrainageDetail:
|
||||
pipes: list[StructureCandidate] = field(default_factory=list)
|
||||
basins: list[WatershedBasin] = field(default_factory=list)
|
||||
grid_cell_m: float = 1.0
|
||||
# B04가 계산해 둔 평균 흐름 화살표를 그대로 넘긴다 — B05는 다시 계산하지 않는다.
|
||||
flow_arrows: list[list[Any]] = field(default_factory=list)
|
||||
arrow_spacing_m: float = 0.0
|
||||
|
||||
|
||||
def build_drainage_detail(
|
||||
@@ -94,6 +98,8 @@ def build_drainage_detail(
|
||||
basin_lonlat=routing.basin_lonlat,
|
||||
pipes=pipes,
|
||||
grid_cell_m=routing.spec.cell_m,
|
||||
flow_arrows=routing.flow_arrows,
|
||||
arrow_spacing_m=routing.arrow_spacing_m,
|
||||
)
|
||||
if not pipes:
|
||||
return detail
|
||||
@@ -140,6 +146,9 @@ class RoadRouting:
|
||||
route_lonlat: list[list[float]] = field(default_factory=list)
|
||||
basin_lonlat: list[list[float]] = field(default_factory=list)
|
||||
base_pipes: list[StructureCandidate] = field(default_factory=list)
|
||||
# 평균 흐름 화살표 — [x, y, 방위(도), 도로도달, 셀 수]. B04가 계산해 둔 그대로.
|
||||
flow_arrows: list[list[Any]] = field(default_factory=list)
|
||||
arrow_spacing_m: float = 0.0
|
||||
|
||||
@property
|
||||
def strength_curve(self) -> np.ndarray:
|
||||
@@ -204,6 +213,9 @@ def _read_geometry(path: Path, routing: RoadRouting) -> None:
|
||||
except (OSError, json.JSONDecodeError):
|
||||
logger.warning("배수유역: B04 기하 산출물을 읽지 못했습니다 (%s).", path)
|
||||
return
|
||||
routing.arrow_spacing_m = float(
|
||||
(document.get("properties") or {}).get("arrow_spacing_m") or 0.0
|
||||
)
|
||||
for feature in document.get("features", []):
|
||||
properties = feature.get("properties") or {}
|
||||
geometry = feature.get("geometry") or {}
|
||||
@@ -213,6 +225,17 @@ def _read_geometry(path: Path, routing: RoadRouting) -> None:
|
||||
routing.route_lonlat = coordinates
|
||||
elif kind == "basin_boundary" and geometry.get("type") == "Polygon" and coordinates:
|
||||
routing.basin_lonlat = coordinates[0]
|
||||
elif kind == "flow_arrow" and geometry.get("type") == "Point":
|
||||
# 화면이 미터로 그리므로 속성의 x·y를 쓴다(기하는 저장 규약상 lon/lat).
|
||||
routing.flow_arrows.append(
|
||||
[
|
||||
float(properties.get("x") or 0.0),
|
||||
float(properties.get("y") or 0.0),
|
||||
float(properties.get("azimuth_deg") or 0.0),
|
||||
bool(properties.get("reaches_road")),
|
||||
int(properties.get("cells") or 0),
|
||||
]
|
||||
)
|
||||
elif kind == "pipe" and geometry.get("type") == "Point":
|
||||
routing.base_pipes.append(
|
||||
StructureCandidate(
|
||||
|
||||
@@ -112,6 +112,9 @@ async def post_drainage_basins(
|
||||
"route_lonlat": detail.route_lonlat,
|
||||
"main_polygon_lonlat": detail.basin_lonlat,
|
||||
"grid_cell_m": detail.grid_cell_m,
|
||||
# 평균 흐름 화살표 — B04가 계산해 저장한 것을 그대로 넘긴다(사업지 CRS m).
|
||||
"flow_arrows": detail.flow_arrows,
|
||||
"arrow_spacing_m": detail.arrow_spacing_m,
|
||||
# 계획선 위 배관 지점 — 유역이 없는 관도 마커로 표시해야 하므로 별도 목록.
|
||||
"pipes": [_candidate_payload(pipe, to_lonlat) for pipe in detail.pipes],
|
||||
"basins": [
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
type DrainageBasin,
|
||||
type RoutePoint,
|
||||
} from "./B05_wf2_Route_Api_Fetch";
|
||||
import { drawFlowArrows, type FlowArrow } from "../B04_wf1_Surface/B04_wf1_Surface_UI_FlowArrows";
|
||||
import { createPipeEditor } from "./B05_wf2_Route_UI_Drainage_Pipes";
|
||||
|
||||
// 배수유역도 패널 — 하단 종단 패널 안쪽 우측에 도킹되는 2단 사이드 패널.
|
||||
@@ -109,7 +110,20 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
autoButton.type = "button";
|
||||
autoButton.className = "b05-drainage__analyze b05-drainage__tool";
|
||||
autoButton.textContent = "자동 제안";
|
||||
header.append(analyzeButton, editButton, deleteButton, autoButton);
|
||||
// 흐름 화살표 보기/숨기기 — 도면이 지저분해질 때 끄기 위한 토글.
|
||||
const arrowButton = document.createElement("button");
|
||||
arrowButton.type = "button";
|
||||
arrowButton.className = "b05-drainage__analyze b05-drainage__tool is-active";
|
||||
arrowButton.textContent = "흐름 화살표";
|
||||
arrowButton.title = "B04에서 산출한 평균 흐름 방향을 보이거나 숨깁니다.";
|
||||
arrowButton.setAttribute("aria-pressed", "true");
|
||||
arrowButton.addEventListener("click", () => {
|
||||
showArrows = !showArrows;
|
||||
arrowButton.classList.toggle("is-active", showArrows);
|
||||
arrowButton.setAttribute("aria-pressed", String(showArrows));
|
||||
scheduleDraw();
|
||||
});
|
||||
header.append(analyzeButton, editButton, deleteButton, autoButton, arrowButton);
|
||||
|
||||
const viewport = document.createElement("div");
|
||||
viewport.className = "b05-drainage__viewport";
|
||||
@@ -147,6 +161,10 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
// 2차 전체 배수유역 외곽선 = 분수령. 별도 "능선" 레이어를 두지 않고 이 선 하나로 표시한다
|
||||
// (전체 유역 외곽선과 능선이 같은 선이므로 — 2026-07-31 사용자 지시).
|
||||
let mainBoundary: Array<[number, number]> = [];
|
||||
// 평균 흐름 화살표 — B04가 계산해 둔 것을 그대로 받아 그린다(여기서 계산하지 않는다).
|
||||
let flowArrows: FlowArrow[] = [];
|
||||
let arrowSpacingM = 0;
|
||||
let showArrows = true;
|
||||
let scale = 1;
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
@@ -230,6 +248,27 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
context.strokeStyle = ROUTE_COLOR;
|
||||
drawPreparedLayer(context, routeLayer, view, "dot");
|
||||
}
|
||||
// 평균 흐름 화살표 — 유역 채움 위, 배관 마커 아래. 좌표는 사업지 CRS(m)라
|
||||
// 도엽 메타로 바로 화면에 옮긴다(배관 마커와 같은 변환).
|
||||
if (showArrows && meta && flowArrows.length > 0) {
|
||||
const spanX = meta.width_meters || 1;
|
||||
const spanY = meta.height_meters || 1;
|
||||
const pxPerMeter = (view.mapRect.width * view.scale) / spanX;
|
||||
const ax = view.mapRect.width * view.scale;
|
||||
const bx = view.mapRect.x * view.scale + (width / 2) * (1 - view.scale) + view.offsetX;
|
||||
const ay = view.mapRect.height * view.scale;
|
||||
const by = view.mapRect.y * view.scale + (height / 2) * (1 - view.scale) + view.offsetY;
|
||||
const originX = meta.x_min;
|
||||
const originY = meta.y_min;
|
||||
drawFlowArrows(
|
||||
context,
|
||||
flowArrows,
|
||||
arrowSpacingM,
|
||||
pxPerMeter,
|
||||
(x, y) => [((x - originX) / spanX) * ax + bx, (1 - (y - originY) / spanY) * ay + by],
|
||||
view,
|
||||
);
|
||||
}
|
||||
// 배관(관 매설) 마커 — 계획선 위 최상단.
|
||||
pipeEditor.draw(context, view, pipeColor);
|
||||
updateImageTransform();
|
||||
@@ -317,6 +356,8 @@ export function createDrainagePanel(): DrainagePanel {
|
||||
const response = await fetchDrainageBasins(projectId, chainages);
|
||||
basins = response.basins;
|
||||
mainBoundary = response.main_polygon_lonlat ?? [];
|
||||
flowArrows = (response.flow_arrows ?? []) as FlowArrow[];
|
||||
arrowSpacingM = response.arrow_spacing_m ?? 0;
|
||||
// 계획도로선·2차 유역 외곽선은 B04 산출물을 그대로 받는다 — 여기서 다시 계산하지 않는다.
|
||||
// 실제 사용된 배관 목록으로 마커를 동기화한다(유역 없는 관 포함).
|
||||
pipeEditor.setPipes(
|
||||
|
||||
Reference in New Issue
Block a user