feat(511-B B-2 서버 측점 생성 = 같은 TS): build_sections(B03 체인 · 노선 저장 · 재생성 · 넓힘 · 추천 초기 종단) = DTM 이면 늘 떠 있는 Node stations.sections(buildStations · sectionsGround · 측점 열쇠 key 더함 · 파이썬은 머리만) · DTM 아닌 지표면 · Node 못 돌림 = 파이썬 generate_sections · stations.list(측점 목록 · 열쇠) · /calc 창구는 stations.sections 안 받음(서버 경로 입력) · 등록부 R5 R6 R7 = node:stations.list · G4 = node:stations.sections · round6 = 십진 반올림(파이썬 round 꼴) · 추천 저장본 지문에 TS 더함 · 고정 자료 Node 길 = 파이썬 길 0 차(다섯 갈래) · golden 그대로 — 요구 320-4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@
|
||||
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } ·
|
||||
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } ·
|
||||
* rebase? · carry?(노선 바뀐 [저장] · `design_calc_node_carry.ts`) ·
|
||||
* stages?(묶음 단계 · `design_calc_node_stages.ts` · 511-B) } }
|
||||
* stages?(묶음 단계 · `design_calc_node_stages.ts` · 511-B) ·
|
||||
* stations?(측점 · `design_calc_node_sections.ts` · 511-B B-2) } }
|
||||
* 출력 { results: { [id]: 값 }, available: [id…] }
|
||||
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
|
||||
* server-calc 묶음과 같은 차례).
|
||||
@@ -43,12 +44,13 @@ import {
|
||||
} from "../../../B06_Section/B06_Section_Structure_Layouts";
|
||||
import { carryRunners, type CarryInputs } from "./design_calc_node_carry";
|
||||
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
|
||||
import { sectionsRunners, type SectionsInputs } from "./design_calc_node_sections";
|
||||
import { STAGE_IDS, stageRunners, type StagesInputs } from "./design_calc_node_stages";
|
||||
import { TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial";
|
||||
|
||||
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
|
||||
|
||||
interface CalcInputs extends FollowInputs, CarryInputs, TrialInputs, StagesInputs {
|
||||
interface CalcInputs extends FollowInputs, CarryInputs, TrialInputs, StagesInputs, SectionsInputs {
|
||||
detail?: SectionDetailResponse;
|
||||
route_points?: RoutePoint[];
|
||||
context?: Partial<
|
||||
@@ -148,6 +150,7 @@ async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
|
||||
...carryRunners(inputs), // 노선 바뀐 [저장] 다시 얹기 — 관 · 구조물 초안 · 횡단 고침(472-6 6-B)
|
||||
...trialRunners(inputs), // 계산 자리 「서버」 시험 길(511-A)
|
||||
...stageRunners(inputs), // 묶음 단계 노선 ~ 횡단(511-B B-1)
|
||||
...sectionsRunners(inputs), // 측점 목록 · 서버 측점 생성(511-B B-2)
|
||||
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
|
||||
"structure.layouts": () => {
|
||||
const areas = structureAreaRows(sections);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/logic/design_calc_node_sections.ts
|
||||
* 계산 Node 입구(`design_calc_node.ts`)의 측점 몫(PLAN 511-B B-2) — 화면 묶음 stations 단계와 같은 TS.
|
||||
*
|
||||
* stations.list 입력 stations { line · curves · pipes · structures · types · definition? · plus ·
|
||||
* interval_m · include_endpoint? · station_snap · station_snap_step } →
|
||||
* 측점 목록(`buildStations` · 열쇠 · 종류 · 반경 · 확폭 · 지반 null) — 계산 자리 「서버」 ·
|
||||
* 노선 저장 옛 노선 열쇠
|
||||
* stations.sections 같은 입력 + grid(DTM 격자 이진 경로) · 표본 간격 → 서버 `generate_sections` 꼴
|
||||
* (`generateSections`) — 서버 측점 생성 길만(창구 `/calc` 는 막음 · 경로 입력)
|
||||
* definition = 구조물 정의(`{types, …정의}` · 화면 `fetchStructureTypes` 와 같은 꼴) — 심는 측점 · 카드 이름 표.
|
||||
* ========================================================================== */
|
||||
|
||||
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
|
||||
import type { PlanPipe, PlanStructure, PlanType } from "@util/common_util_structure_station";
|
||||
import { buildStations } from "../../../B05_Profile/logic/B05_Profile_Sections_Stations";
|
||||
import { generateSections } from "../../../B05_Profile/logic/B05_Profile_Sections_Generate";
|
||||
import type { DesignCurve, Vertex } from "./design_pipeline_types";
|
||||
import { gridAt } from "./design_calc_node_stages";
|
||||
|
||||
export interface SectionsInputs {
|
||||
stations?: {
|
||||
line: Vertex[];
|
||||
curves?: DesignCurve[];
|
||||
pipes?: PlanPipe[];
|
||||
structures?: PlanStructure[];
|
||||
types: PlanType[];
|
||||
definition?: CatalogDefinition | null;
|
||||
plus?: number[];
|
||||
interval_m?: number;
|
||||
include_endpoint?: boolean;
|
||||
station_snap?: boolean;
|
||||
station_snap_step?: number | null;
|
||||
grid?: string;
|
||||
cross_half_width_m?: number;
|
||||
cross_sample_interval_m?: number;
|
||||
long_sample_interval_m?: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** id → 셈(입구 `RUNNERS` 에 얹음). */
|
||||
export function sectionsRunners(inputs: SectionsInputs): Record<string, () => unknown> {
|
||||
const args = () => {
|
||||
const at = inputs.stations;
|
||||
if (!at) throw new Error("측점 입력(stations) 없음");
|
||||
if (at.definition !== undefined) useCatalogDefinition(at.definition);
|
||||
const options = {
|
||||
intervalM: at.interval_m,
|
||||
includeEndpoint: at.include_endpoint,
|
||||
stationSnap: at.station_snap ?? false,
|
||||
stationSnapStep: at.station_snap_step ?? null,
|
||||
types: at.types,
|
||||
plus: at.plus ?? [],
|
||||
crossHalfWidthM: at.cross_half_width_m,
|
||||
crossSampleIntervalM: at.cross_sample_interval_m,
|
||||
longSampleIntervalM: at.long_sample_interval_m,
|
||||
};
|
||||
return { at, options };
|
||||
};
|
||||
return {
|
||||
"stations.list": () => {
|
||||
const { at, options } = args();
|
||||
return buildStations(at.line, at.curves ?? [], at.pipes ?? [], at.structures ?? [], options);
|
||||
},
|
||||
"stations.sections": () => {
|
||||
const { at, options } = args();
|
||||
if (!at.grid) throw new Error("DTM 격자 없음 — DTM 아닌 지표면은 파이썬 길");
|
||||
const grid = gridAt(at.grid);
|
||||
return generateSections(
|
||||
at.line,
|
||||
at.curves ?? [],
|
||||
at.pipes ?? [],
|
||||
at.structures ?? [],
|
||||
grid,
|
||||
options,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const STAGE_IDS = STAGE_NAMES.map((name) => `stage.${name}`);
|
||||
let gridMemo: { path: string; mtime: number; grid: DtmGrid } | null = null;
|
||||
|
||||
/** DTM 격자 이진 — 같은 경로 · 같은 수정 시각이면 앞에 읽은 것(늘 떠 있는 Node). */
|
||||
function gridAt(path: string): DtmGrid {
|
||||
export function gridAt(path: string): DtmGrid {
|
||||
const mtime = statSync(path).mtimeMs;
|
||||
if (gridMemo?.path !== path || gridMemo.mtime !== mtime) {
|
||||
const bytes = readFileSync(path);
|
||||
|
||||
@@ -121,10 +121,14 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "stations",
|
||||
ts: `${B05L}B05_Profile_UI_RouteEdit_Frame.ts#stationFrameAt`,
|
||||
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
until: "472-4",
|
||||
runner: "node:stations.list",
|
||||
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
mirror: ["test_b05_routeedit_frame_mirror", "test_224_2_station_snap"],
|
||||
mirror: [
|
||||
"test_b05_routeedit_frame_mirror",
|
||||
"test_224_2_station_snap",
|
||||
"test_511_b2_stations_node",
|
||||
],
|
||||
note: "서버 측점 생성 = Node `stations.sections`(DTM) · 파이썬 짝 = DTM 아닌 지표면 · 추천 엔진 · B06 측점 맞춤",
|
||||
},
|
||||
{
|
||||
id: "stations.list",
|
||||
@@ -143,11 +147,14 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "stations",
|
||||
ts: `${B05L}B05_Profile_Sections_Stations.ts#buildStations`,
|
||||
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
until: "472-4",
|
||||
runner: "node:stations.list",
|
||||
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
mirror: ["test_344_3_pipeline_stations_mirror", "test_407_curve_station"],
|
||||
note: "파이썬 짝은 B03 체인 · B07 IP 표 · 추천도 부름",
|
||||
mirror: [
|
||||
"test_344_3_pipeline_stations_mirror",
|
||||
"test_407_curve_station",
|
||||
"test_511_b2_stations_node",
|
||||
],
|
||||
note: "서버 측점 생성 길(B03 체인 · 노선 저장 · 재생성 · 추천 초기 종단) = Node `stations.sections`(DTM) · 파이썬 짝 = DTM 아닌 지표면 · B07 IP 표 · 추천 엔진",
|
||||
},
|
||||
{
|
||||
id: "stations.key",
|
||||
@@ -158,11 +165,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "stations",
|
||||
ts: `${U}common_util_station_key.ts#stationKey`,
|
||||
runner: `py:${U}common_util_station_key.py#station_key`,
|
||||
until: "472-4",
|
||||
runner: "node:stations.list",
|
||||
pair: `${U}common_util_station_key.py#station_key`,
|
||||
mirror: ["test_344_3_station_key_mirror"],
|
||||
note: "서버 열쇠(Route_Save_Parts.station_keys)는 주인 = 파이썬 plan_stations(F3 · 카탈로그) · 부르는 곳 = 노선 저장 이월(③) — ③ · F3 와 같이 Node 로",
|
||||
mirror: ["test_344_3_station_key_mirror", "test_511_b2_stations_node"],
|
||||
note: "노선 저장 이월 열쇠 = Node 측점 출력(`key` · 옛 노선 = `stations.list`) · 파이썬 짝 = 옛 열쇠 꼴 읽기(at: · 누가거리)",
|
||||
},
|
||||
{
|
||||
id: "stations.swap",
|
||||
@@ -245,10 +251,10 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [
|
||||
site: "browser_now",
|
||||
stage: "cross",
|
||||
ts: `${B05L}B05_Profile_Pipeline_Detail.ts#uphillSide`,
|
||||
runner: `py:${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
until: "472-4",
|
||||
runner: "node:stations.sections",
|
||||
pair: `${B05}B05_Profile_Engine_Sections_Core.py#generate_sections`,
|
||||
mirror: ["test_346_2_pipeline_cross_page"],
|
||||
mirror: ["test_346_2_pipeline_cross_page", "test_511_b2_stations_node"],
|
||||
note: "서버 측점 생성(DTM) = Node `stations.sections` 의 상단측 · DTM 아닌 지표면 = 파이썬 짝",
|
||||
},
|
||||
/* ── 종단 ─────────────────────────────────────────────────────────────── */
|
||||
{
|
||||
|
||||
@@ -40,6 +40,13 @@ SOURCE_GLOBS = (
|
||||
"common_util/common_util_structure_station.py",
|
||||
"common_util/common_util_drainage_pipes.py",
|
||||
"config/config_system_design.py",
|
||||
# 측점 · 지반 = Node `stations.sections`(511-B B-2) — 묶음을 낳는 TS
|
||||
"B05_Profile/logic/B05_Profile_Sections_*.ts",
|
||||
"B05_Profile/logic/B05_Profile_UI_RouteEdit_Frame.ts",
|
||||
"common_util/common_util_structure_station.ts",
|
||||
"common_util/common_util_structure_catalog.ts",
|
||||
"common_util/common_util_surface_sampler.ts",
|
||||
"A00_Common/design/logic/design_calc_node_sections.ts",
|
||||
)
|
||||
# 직선 위 꼭짓점(438 직선화) 판정 — 앞뒤 기울기 차(비율)
|
||||
_STRAIGHT_EPS = 1e-5
|
||||
@@ -126,12 +133,19 @@ def initial_longitudinal(
|
||||
) -> dict[str, Any]:
|
||||
"""[적용] 체인이 남길 종단(측점 · 계획선) — 측점 생성 · 균형 얹기와 같은 조각."""
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import build_sections, resolve_extra_stations
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
build_sections,
|
||||
resolve_extra_stations,
|
||||
station_facilities,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router_RouteInitial import seat_base_pvi
|
||||
|
||||
polyline = [[x, y, 0.0] for x, y in line]
|
||||
pipes, structures, plus = facilities
|
||||
extras = resolve_extra_stations(project_root, pipes, polyline, structures=structures, plus=plus)
|
||||
planted = station_facilities(project_root, pipes, polyline, structures=structures, plus=plus)
|
||||
extras = resolve_extra_stations(
|
||||
project_root, planted[0], structures=planted[1], plus=planted[2]
|
||||
)
|
||||
result, _summary = build_sections(
|
||||
polyline,
|
||||
sampler,
|
||||
@@ -144,6 +158,7 @@ def initial_longitudinal(
|
||||
design_curves=curves,
|
||||
station_snap=True, # [적용] = 스냅 켬 · 측점 No.(`saveRecommendedRoute`)
|
||||
station_snap_step=None,
|
||||
facilities=planted,
|
||||
)
|
||||
longitudinal = result["longitudinal"]
|
||||
if target is not None and longitudinal.get("profile_alignment"):
|
||||
|
||||
@@ -34,6 +34,7 @@ from B05_Profile.B05_Profile_Engine_Sections_Names import ( # noqa: F401 —
|
||||
prune_stale_cross_files,
|
||||
valid_cross_names,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Node import node_sections
|
||||
from B05_Profile.B05_Profile_Structures_Repository import load_structures
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
structure_type_map,
|
||||
@@ -257,6 +258,24 @@ def resolve_extra_stations(
|
||||
`xy` = 지금 노선 — 노선 밖(시점 앞 · 종점 뒤 · `route_outside`) 관 · 구조물은 뺌(459).
|
||||
`structures` · `plus` = 파일 대신 따라간 사본(476 추천 초기 종단 · 파일 안 씀).
|
||||
"""
|
||||
return tuple(
|
||||
(float(station["chainage_m"]), str(station["title"]))
|
||||
for station in plan_stations(
|
||||
*station_facilities(project_root, pipes, xy, structures=structures, plus=plus)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def station_facilities(
|
||||
project_root: Path,
|
||||
pipes: list[PipePoint],
|
||||
xy: Sequence[Sequence[float]] | None = None,
|
||||
*,
|
||||
structures: list[Any] | None = None,
|
||||
plus: list[float] | None = None,
|
||||
) -> tuple[list[PipePoint], list[Any], list[float]]:
|
||||
"""측점을 심는 관 · 구조물 · 추가 측점(노선 밖 뺌) — 파이썬 `plan_stations` ·
|
||||
Node 측점 생성이 같은 입력."""
|
||||
from B05_Profile.B05_Profile_Plus_Stations import plus_chainages
|
||||
from common_util.common_util_route_follow import route_outside
|
||||
|
||||
@@ -273,12 +292,7 @@ def resolve_extra_stations(
|
||||
for s in structures
|
||||
if not route_outside(getattr(s, "x", None), getattr(s, "y", None), xy)
|
||||
]
|
||||
return tuple(
|
||||
(float(station["chainage_m"]), str(station["title"]))
|
||||
for station in plan_stations(
|
||||
pipes, structures, plus_chainages(project_root) if plus is None else plus
|
||||
)
|
||||
)
|
||||
return pipes, structures, plus_chainages(project_root) if plus is None else list(plus)
|
||||
|
||||
|
||||
def _append_design_profiles(
|
||||
@@ -408,21 +422,28 @@ def build_sections(
|
||||
design_curves: list[dict[str, Any]],
|
||||
station_snap: bool,
|
||||
station_snap_step: float | None,
|
||||
facilities: tuple[list[Any], list[Any], list[float]] | None = None,
|
||||
) -> tuple[dict[str, Any], dict[str, Any] | None]:
|
||||
"""측점 틀 · 지반 · 1차 계획선(438) · 포장 제안 — 파일 안 씀 · (결과, 계획선 요약).
|
||||
|
||||
종횡단 생성(`run_section_generation`)과 추천 후보 초기 종단(476 · `..._RouteInitial_Initial`)
|
||||
이 같은 셈 한 벌."""
|
||||
result = generate_sections(
|
||||
polyline,
|
||||
sampler,
|
||||
replace(options or SectionGenerationOptions(), extra_stations=extras),
|
||||
source_snapshot=source_snapshot,
|
||||
crs=crs,
|
||||
design_curves=design_curves,
|
||||
station_snap=station_snap,
|
||||
station_snap_step=station_snap_step,
|
||||
)
|
||||
이 같은 셈 한 벌. 측점 · 지반 = 화면과 같은 TS(Node `stations.sections` · 511-B B-2 ·
|
||||
`facilities` = `station_facilities`) · DTM 아닌 지표면 · Node 못 돌림 =
|
||||
파이썬 `generate_sections`."""
|
||||
generation = replace(options or SectionGenerationOptions(), extra_stations=extras)
|
||||
shared = {
|
||||
"crs": crs,
|
||||
"design_curves": design_curves,
|
||||
"station_snap": station_snap,
|
||||
"station_snap_step": station_snap_step,
|
||||
}
|
||||
result = (
|
||||
node_sections(
|
||||
polyline, sampler, facilities, generation, source_snapshot=source_snapshot, **shared
|
||||
)
|
||||
if facilities is not None
|
||||
else None
|
||||
) or generate_sections(polyline, sampler, generation, source_snapshot=source_snapshot, **shared)
|
||||
grade_summary = _append_design_profiles(
|
||||
result["longitudinal"],
|
||||
grade_options,
|
||||
@@ -475,10 +496,13 @@ def run_section_generation(
|
||||
# 값이라, 되먹이면 B04에서 지운 관이 파일 정본을 이기고 되살아난다.
|
||||
pipes = _load_pipe_points(project_root, polyline)
|
||||
try:
|
||||
extras = resolve_extra_stations(project_root, pipes, polyline)
|
||||
facilities = station_facilities(project_root, pipes, polyline)
|
||||
extras = resolve_extra_stations(
|
||||
project_root, facilities[0], structures=facilities[1], plus=facilities[2]
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B05 구조물 측점 해석 실패 (규칙 격자만 생성)")
|
||||
extras = ()
|
||||
facilities, extras = ([], [], []), ()
|
||||
logger.info("B05 종횡단: 구조물 측점 %d건 파생(관 %d건)", len(extras), len(pipes))
|
||||
|
||||
result, grade_summary = build_sections(
|
||||
@@ -495,6 +519,7 @@ def run_section_generation(
|
||||
design_curves=_load_design_curves(project_root),
|
||||
station_snap=load_station_snap(project_root),
|
||||
station_snap_step=load_station_snap_step(project_root),
|
||||
facilities=facilities,
|
||||
)
|
||||
|
||||
stage_root = project_root / _STAGE_SUBDIR
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
(`buildStations`). 거울 시험 `resources/tester/test_344_3_pipeline_stations_mirror.py`.
|
||||
⚠ 파이썬·TS 짝 — `generate_sections` 상단측(`uphill_side`) ↔ `B05_Profile/logic/B05_Profile_Pipeline_Detail.ts`
|
||||
(`uphillSide`). 거울 시험 `resources/tester/test_346_2_pipeline_cross_page.py`.
|
||||
서버 측점 생성 길(`build_sections`)은 DTM 이면 같은 TS(Node `stations.sections` · 511-B B-2 ·
|
||||
`B05_Profile_Engine_Sections_Node.py`) — 이 짝을 쓰는 곳 = DTM 아닌 지표면 · Node 못 돌림 ·
|
||||
구조물 측점만 뜨기(`sample_structure_sections`) · B06 곡선 측점 판정 · 추천 엔진(`_plan_radii` 등).
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""서버 측점 생성 = 화면과 같은 TS(PLAN 511-B B-2 · 요구 320-4) — 늘 떠 있는 Node
|
||||
`stations.sections`.
|
||||
|
||||
`build_sections`(B03 체인 · 노선 저장 · 재생성 · 넓힘 · 추천 초기 종단)가 부름 — 측점 자리 ·
|
||||
종류 · 열쇠 · 반경 · 확폭 · 지반 · 틀 · 상단측 =
|
||||
`B05_Profile/logic/B05_Profile_Sections_Generate.ts`
|
||||
(화면 묶음 stations · ground 단계와 같은 함수). 파이썬은 머리(schema_version · source ·
|
||||
coordinate_reference · cad_exchange · options)만 붙임.
|
||||
|
||||
DTM 지표면만(격자 이진 = `dtm_grid_file`) — DTM 아닌 지표면(tin · nurbs · implicit · meshfree) ·
|
||||
Node 못 돌림 = None → 부르는 쪽이 파이썬 `generate_sections`(짝 · 로그 남김).
|
||||
측점 열쇠(`key`) = 노선 저장 이월 열쇠(`B05_Profile_Route_Save._carry_cross`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import (
|
||||
SECTION_SCHEMA_VERSION,
|
||||
SectionGenerationOptions,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def plan_pipe(pipe: Any) -> dict[str, Any]:
|
||||
"""관 정본 → TS `PlanPipe`(누가거리 그대로 · 파이썬 `plan_stations` 가 읽는 칸)."""
|
||||
return {
|
||||
"id": getattr(pipe, "id", None),
|
||||
"chainage_m": float(pipe.chainage_m),
|
||||
"facility": getattr(pipe, "facility", None) or "pipe",
|
||||
"start_m": getattr(pipe, "start_m", None),
|
||||
"end_m": getattr(pipe, "end_m", None),
|
||||
"options": getattr(pipe, "options", None) or {},
|
||||
}
|
||||
|
||||
|
||||
def structure_table() -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||
"""구조물 종류(`/projects/structure-types` 꼴) · 정의(화면 `useCatalogDefinition` 꼴)."""
|
||||
from B05_Profile.B05_Profile_Structures_Schema import _read_definition, _stamp
|
||||
from M02_MasterTemplete.StructOptions.M02_SO_Options import with_visible
|
||||
|
||||
types = [item.model_dump(mode="json") for item in with_visible().types]
|
||||
return types, {"types": types, **_read_definition(*_stamp())}
|
||||
|
||||
|
||||
def stations_input(
|
||||
xy: list[list[float]],
|
||||
pipes: list[Any],
|
||||
structures: list[Any],
|
||||
plus: list[float],
|
||||
options: SectionGenerationOptions,
|
||||
design_curves: list[dict[str, Any]],
|
||||
station_snap: bool,
|
||||
station_snap_step: float | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Node `stations.list` · `stations.sections` 입력(격자 빼고)."""
|
||||
types, definition = structure_table()
|
||||
return {
|
||||
"line": xy,
|
||||
"curves": design_curves,
|
||||
"pipes": [plan_pipe(pipe) for pipe in pipes],
|
||||
"structures": [
|
||||
item.model_dump(mode="json") if hasattr(item, "model_dump") else dict(item)
|
||||
for item in structures
|
||||
],
|
||||
"types": types,
|
||||
"definition": definition,
|
||||
"plus": [float(at) for at in plus],
|
||||
"interval_m": options.station_interval_m,
|
||||
"include_endpoint": options.include_endpoint,
|
||||
"station_snap": station_snap,
|
||||
"station_snap_step": station_snap_step,
|
||||
"cross_half_width_m": options.cross_half_width_m,
|
||||
"cross_sample_interval_m": options.cross_sample_interval_m,
|
||||
"long_sample_interval_m": options.long_sample_interval_m,
|
||||
}
|
||||
|
||||
|
||||
def node_stations(stations: dict[str, Any]) -> list[dict[str, Any]] | None:
|
||||
"""측점 목록(`buildStations` · 열쇠 · 누가거리 · 종류) — 못 돌림 None."""
|
||||
from common_util.common_util_calc_runner import run_calc_served
|
||||
|
||||
answer = run_calc_served(["stations.list"], {"stations": stations})
|
||||
if not answer or "error" in answer:
|
||||
logger.warning("Node 측점 목록 실패: %s", (answer or {}).get("error"))
|
||||
return None
|
||||
return answer["results"]["stations.list"]
|
||||
|
||||
|
||||
def node_sections(
|
||||
polyline: list[list[float]],
|
||||
sampler: Any,
|
||||
facilities: tuple[list[Any], list[Any], list[float]],
|
||||
options: SectionGenerationOptions,
|
||||
*,
|
||||
source_snapshot: dict[str, Any],
|
||||
crs: str | None,
|
||||
design_curves: list[dict[str, Any]],
|
||||
station_snap: bool,
|
||||
station_snap_step: float | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""`generate_sections` 와 같은 꼴(+ 측점 `key`) — DTM 아님 · Node 못 돌림 = None."""
|
||||
from common_util.common_util_calc_runner import run_calc_served
|
||||
from common_util.common_util_surface_sampler import dtm_grid_file
|
||||
|
||||
source = getattr(sampler, "source", None)
|
||||
if not source:
|
||||
return None
|
||||
options.validate()
|
||||
pipes, structures, plus = facilities
|
||||
stations = stations_input(
|
||||
[[float(p[0]), float(p[1])] for p in polyline],
|
||||
pipes,
|
||||
structures,
|
||||
plus,
|
||||
options,
|
||||
design_curves,
|
||||
station_snap,
|
||||
station_snap_step,
|
||||
)
|
||||
stations["grid"] = str(dtm_grid_file(source))
|
||||
answer = run_calc_served(["stations.sections"], {"stations": stations})
|
||||
if not answer or "error" in answer:
|
||||
logger.warning("Node 측점 생성 실패 — 파이썬 길: %s", (answer or {}).get("error"))
|
||||
return None
|
||||
out = answer["results"]["stations.sections"]
|
||||
return {
|
||||
"schema_version": SECTION_SCHEMA_VERSION,
|
||||
"status": "completed",
|
||||
"source": source_snapshot or {},
|
||||
"coordinate_reference": {
|
||||
"crs": crs,
|
||||
"world_axes": {"x": "project_easting", "y": "project_northing", "z": "elevation"},
|
||||
"units": {"horizontal": "m", "vertical": "m", "angle": "degree"},
|
||||
},
|
||||
"cad_exchange": {
|
||||
"station_origin": "BP",
|
||||
"chainage_direction": "BP_to_EP",
|
||||
"cross_offset_sign": {"negative": "right", "positive": "left"},
|
||||
"cross_local_axes": {"x": "offset_m", "y": "elevation_m"},
|
||||
"recommended_drawing_datum_m": out["datum"],
|
||||
},
|
||||
"options": {
|
||||
"station_interval_m": options.station_interval_m,
|
||||
"cross_half_width_m": options.cross_half_width_m,
|
||||
"cross_sample_interval_m": options.cross_sample_interval_m,
|
||||
"long_sample_interval_m": options.long_sample_interval_m,
|
||||
"include_endpoint": options.include_endpoint,
|
||||
"extra_stations": [list(entry) for entry in options.extra_stations],
|
||||
},
|
||||
"longitudinal": out["longitudinal"],
|
||||
"cross_sections": out["cross_sections"],
|
||||
"summary": out["summary"],
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/* =============================================================================
|
||||
* B05_Profile/logic/B05_Profile_Sections_Generate.ts
|
||||
* 측점 생성 한 벌(PLAN 511-B B-2) — 노선 + 관 · 구조물 · 추가 측점 + DTM 격자 → 서버 `generate_sections`
|
||||
* 꼴(종단 표본 · 측점 · 횡단 표본). 서버 측점 생성 길(`B05_Profile_Engine_Sections_Node.py` → Node
|
||||
* `stations.sections`)이 부름 — 화면 묶음과 같은 측점(`buildStations`) · 같은 지반(`sectionsGround`).
|
||||
*
|
||||
* 측점 = `buildStations`(열쇠 · 종류 · 반경 · 확폭) · 지반 · 틀 · 상단측 = `sectionsGround` · 이름 =
|
||||
* `plantedStations`(카드 이름) · 칸 차례 · 자릿수 = 파이썬 `generate_sections` 그대로 + 측점 열쇠 `key`.
|
||||
* 머리(schema_version · source · coordinate_reference · cad_exchange · options)는 파이썬이 붙임.
|
||||
* 순수 — 화면 · fetch · 세션 없음.
|
||||
* ========================================================================== */
|
||||
|
||||
import type { DtmGrid } from "../../common_util/common_util_surface_sampler";
|
||||
import { stationsAlong } from "./B05_Profile_UI_RouteEdit_Frame";
|
||||
import { formatStation } from "../../B06_Section/logic/B06_Section_Station_Swap";
|
||||
import { stationIdOf } from "./B05_Profile_Pipeline_Grade";
|
||||
import {
|
||||
buildStations,
|
||||
lineLength,
|
||||
plantedStations,
|
||||
SECTION_INCLUDE_ENDPOINT,
|
||||
SECTION_STATION_INTERVAL_M,
|
||||
type StationsOptions,
|
||||
} from "./B05_Profile_Sections_Stations";
|
||||
import { sectionsGround, type GroundOptions } from "./B05_Profile_Sections_Ground";
|
||||
import type { PlanPipe, PlanStructure } from "../../common_util/common_util_structure_station";
|
||||
import type { DesignCurve, Vertex } from "../../A00_Common/design/logic/design_pipeline_types";
|
||||
|
||||
export type GenerateOptions = Omit<StationsOptions, "registry"> & GroundOptions;
|
||||
|
||||
const KIND: Record<string, string> = {
|
||||
grid: "regular",
|
||||
curve: "curve",
|
||||
pipe: "irregular",
|
||||
structure: "irregular",
|
||||
};
|
||||
|
||||
/** 서버 `generate_sections` 꼴 몸통 — 종단(길이 · 표본 · 측점) · 횡단 · 요약 · 도면 기준고. */
|
||||
export function generateSections(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
curves: ReadonlyArray<DesignCurve>,
|
||||
pipes: ReadonlyArray<PlanPipe>,
|
||||
structures: ReadonlyArray<PlanStructure>,
|
||||
grid: DtmGrid,
|
||||
options: GenerateOptions,
|
||||
) {
|
||||
const stations = buildStations(line, curves, pipes, structures, options);
|
||||
if (stations.length < 2) throw new Error("경로는 최소 2개의 (x, y, z) 좌표로 구성되어야 합니다.");
|
||||
const gridM = stationsAlong(
|
||||
line,
|
||||
options.intervalM ?? SECTION_STATION_INTERVAL_M,
|
||||
options.includeEndpoint ?? SECTION_INCLUDE_ENDPOINT,
|
||||
);
|
||||
const planted = plantedStations(gridM, lineLength(line), pipes, structures, options);
|
||||
const chainages = stations.map((station) => station.chainageM);
|
||||
const ground = sectionsGround(line, chainages, grid, options);
|
||||
const crossSections = stations.map((station, index) => {
|
||||
const at = ground.stations[index];
|
||||
const mark = [...planted].find(([m]) => Math.abs(m - station.chainageM) < 1e-6)?.[1];
|
||||
const kind = index === 0 ? "bp" : station.kind === "end" ? "ep" : KIND[station.kind];
|
||||
const head: Record<string, unknown> = {
|
||||
station_id: stationIdOf(station.chainageM),
|
||||
chainage_m: at.chainage_m,
|
||||
label: formatStation(station.chainageM) + (kind === "curve" ? " (MC)" : ""),
|
||||
kind,
|
||||
center_x: at.center_x,
|
||||
center_y: at.center_y,
|
||||
center_z: at.center_z,
|
||||
azimuth_deg: station.azimuthDeg,
|
||||
plan_radius_m: station.planRadiusM,
|
||||
curve_outer_side: station.curveOuterSide,
|
||||
curve_widening_m: station.curveWideningM,
|
||||
uphill_side: at.uphill_side,
|
||||
frame: {
|
||||
origin: { x: at.center_x, y: at.center_y, z: at.center_z },
|
||||
tangent_xy: at.tangent_xy,
|
||||
left_xy: at.left_xy,
|
||||
up_xyz: [0.0, 0.0, 1.0],
|
||||
},
|
||||
};
|
||||
if (mark) {
|
||||
head.structure = mark.title;
|
||||
if (gridM.some((m) => Math.abs(m - station.chainageM) < 1e-6)) head.grid = true;
|
||||
}
|
||||
head.key = station.key;
|
||||
return { head, samples: at.samples };
|
||||
});
|
||||
const z = ground.samples.flatMap((sample) => (sample.z === null ? [] : [sample.z]));
|
||||
return {
|
||||
longitudinal: {
|
||||
length_m: ground.length_m,
|
||||
samples: ground.samples,
|
||||
stations: crossSections.map(({ head }) => head),
|
||||
},
|
||||
cross_sections: crossSections.map(({ head, samples }) => ({ ...head, samples })),
|
||||
summary: {
|
||||
station_count: stations.length,
|
||||
cross_sample_count: crossSections.reduce((sum, { samples }) => sum + samples.length, 0),
|
||||
invalid_longitudinal_samples: ground.samples.filter((sample) => !sample.valid).length,
|
||||
invalid_cross_samples: crossSections.reduce(
|
||||
(sum, { samples }) => sum + samples.filter((sample) => !sample.valid).length,
|
||||
0,
|
||||
),
|
||||
},
|
||||
datum: z.length ? Math.floor(Math.min(...z) / 10) * 10 : null,
|
||||
};
|
||||
}
|
||||
@@ -69,9 +69,9 @@ export interface GroundOptions {
|
||||
longSampleIntervalM?: number;
|
||||
}
|
||||
|
||||
/** 파이썬 `round(x, 6)` 자리. */
|
||||
export const round6 = (value: number): number => Math.round(value * 1e6) / 1e6;
|
||||
const round9 = (value: number): number => Math.round(value * 1e9) / 1e9;
|
||||
/** 파이썬 `round(x, 6)` 자리 — 십진 자릿수 반올림(곱셈 반올림 오차 없음 · 511-B B-2). */
|
||||
export const round6 = (value: number): number => Number(value.toFixed(6));
|
||||
const round9 = (value: number): number => Number(value.toFixed(9));
|
||||
|
||||
function sample(grid: DtmGrid, x: number, y: number): GroundSample {
|
||||
const z = heightAt(grid, x, y);
|
||||
|
||||
@@ -239,31 +239,31 @@ export interface StationsOptions {
|
||||
}
|
||||
|
||||
/** 노선 길이(m) — 길이 0 선분은 0(서버 `_clean_polyline`). */
|
||||
const lineLength = (line: ReadonlyArray<Vertex>): number =>
|
||||
export const lineLength = (line: ReadonlyArray<Vertex>): number =>
|
||||
line.reduce(
|
||||
(sum, vertex, index) =>
|
||||
index ? sum + Math.hypot(vertex[0] - line[index - 1][0], vertex[1] - line[index - 1][1]) : 0,
|
||||
0,
|
||||
);
|
||||
|
||||
/** 측점 한 벌 — 서버 `generate_sections` 측점 목록과 같은 자리 · 종류 · 틀 · 반경 · 확폭(지반 null). */
|
||||
export function buildStations(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
curves: ReadonlyArray<DesignCurve>,
|
||||
/** 심는 측점 한 자리 — 주인 종류 · id · 역할 · 카드 이름(서버 측점 `structure` 칸). */
|
||||
export interface PlantedMark {
|
||||
kind: "pipe" | "structure";
|
||||
id: string;
|
||||
role: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
/** 심는 측점(관 · 구조물) — 격자 0.005 m 안이면 격자 자리에 얹고 서로 겹치면 앞선 것만(서버 `extras`). */
|
||||
export function plantedStations(
|
||||
grid: ReadonlyArray<number>,
|
||||
total: number,
|
||||
pipes: ReadonlyArray<PlanPipe>,
|
||||
structures: ReadonlyArray<PlanStructure>,
|
||||
options: StationsOptions,
|
||||
): PipelineStation[] {
|
||||
if (line.length < 2) return [];
|
||||
const intervalM = options.intervalM ?? SECTION_STATION_INTERVAL_M;
|
||||
const total = lineLength(line);
|
||||
// 일반측점은 스냅이 옮기지 않음(요구 280-4) — MC 는 아래 곡선 측점으로 더함
|
||||
const grid = stationsAlong(line, intervalM, options.includeEndpoint ?? SECTION_INCLUDE_ENDPOINT);
|
||||
const spans = designSpans(line, curves);
|
||||
|
||||
// 심는 측점(관 · 구조물) — 격자 0.005 m 안이면 격자 자리에 얹고 서로 겹치면 앞선 것만(서버 `extras`).
|
||||
options: Pick<StationsOptions, "types" | "registry" | "plus">,
|
||||
): Map<number, PlantedMark> {
|
||||
const pipeIds = new Set(pipes.map((pipe) => pipe.id ?? `pipe-${pipe.chainage_m.toFixed(2)}`));
|
||||
const planted = new Map<number, { kind: "pipe" | "structure"; id: string; role: string }>();
|
||||
const planted = new Map<number, PlantedMark>();
|
||||
for (const station of planStations(
|
||||
pipes,
|
||||
structures,
|
||||
@@ -280,8 +280,27 @@ export function buildStations(
|
||||
kind: pipeIds.has(owner) ? "pipe" : "structure",
|
||||
id: owner,
|
||||
role: station.role,
|
||||
title: station.title,
|
||||
});
|
||||
}
|
||||
return planted;
|
||||
}
|
||||
|
||||
/** 측점 한 벌 — 서버 `generate_sections` 측점 목록과 같은 자리 · 종류 · 틀 · 반경 · 확폭(지반 null). */
|
||||
export function buildStations(
|
||||
line: ReadonlyArray<Vertex>,
|
||||
curves: ReadonlyArray<DesignCurve>,
|
||||
pipes: ReadonlyArray<PlanPipe>,
|
||||
structures: ReadonlyArray<PlanStructure>,
|
||||
options: StationsOptions,
|
||||
): PipelineStation[] {
|
||||
if (line.length < 2) return [];
|
||||
const intervalM = options.intervalM ?? SECTION_STATION_INTERVAL_M;
|
||||
const total = lineLength(line);
|
||||
// 일반측점은 스냅이 옮기지 않음(요구 280-4) — MC 는 아래 곡선 측점으로 더함
|
||||
const grid = stationsAlong(line, intervalM, options.includeEndpoint ?? SECTION_INCLUDE_ENDPOINT);
|
||||
const spans = designSpans(line, curves);
|
||||
const planted = plantedStations(grid, total, pipes, structures, options);
|
||||
const withPlanted = [...new Set([...grid, ...planted.keys()])];
|
||||
// 곡선 측점(252-2) — MC 에 측점이 없으면 더함 · 값 자릿수 = 스냅 간격(스냅 끔 = 정수 m) · 격자 번호 없음
|
||||
const curved = curveStations(
|
||||
|
||||
@@ -16,7 +16,7 @@ from uuid import UUID
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from common_util.common_util_calc_runner import run_calc_served
|
||||
from common_util.common_util_calc_runner import SERVER_ONLY_IDS, run_calc_served
|
||||
|
||||
router = APIRouter(prefix="/api/projects", tags=["B06 Calc Server"])
|
||||
|
||||
@@ -48,6 +48,8 @@ async def run_calc_endpoint(project_id: UUID, request: Request) -> dict | JSONRe
|
||||
return _error(400, "run = id 목록")
|
||||
if not isinstance(inputs, dict):
|
||||
return _error(400, "inputs = 객체")
|
||||
if SERVER_ONLY_IDS.intersection(run):
|
||||
return _error(400, "서버 측점 생성은 창구 밖")
|
||||
try:
|
||||
answer = await asyncio.to_thread(run_calc_served, run, inputs)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -44,8 +44,12 @@ NODE_IDS = frozenset(
|
||||
"cross.berm_spans",
|
||||
"facility.rebase",
|
||||
"stations.carry",
|
||||
"stations.list",
|
||||
"stations.sections",
|
||||
}
|
||||
)
|
||||
# 창구(`/calc`)가 안 받는 id — 서버 파일 경로를 입력으로 받음(측점 생성 · 511-B B-2)
|
||||
SERVER_ONLY_IDS = frozenset({"stations.sections"})
|
||||
# 시험 길(`design_calc_node_trial.ts` `TRIAL` 표와 같음) — 조작 중 브라우저 함수를 서버가 같은 TS 로
|
||||
TRIAL_IDS = frozenset(
|
||||
{
|
||||
|
||||
@@ -131,7 +131,9 @@ class DtmGridSampler:
|
||||
# ponytail: 지표면 4벌(sheet 격자 한 벌 약 17 MB) — 서버 프로세스 하나 전제 · 늘면 maxsize 를 셈.
|
||||
@lru_cache(maxsize=4)
|
||||
def _open_dtm(path: str, _mtime_ns: int, _size: int) -> DtmGridSampler:
|
||||
return DtmGridSampler.from_npz(path)
|
||||
sampler = DtmGridSampler.from_npz(path)
|
||||
sampler.source = path # npz 경로 — 서버 측점 생성이 같은 격자를 Node 에 넘김(511-B B-2)
|
||||
return sampler
|
||||
|
||||
|
||||
#: 브라우저로 보내는 격자 꼴(`dtm_grid_bytes`) 머리 — TS `parseDtmGrid` 가 같은 값을 봄.
|
||||
|
||||
@@ -48,6 +48,8 @@ NODE_IDS = {
|
||||
"cross.berm_spans",
|
||||
"facility.rebase",
|
||||
"stations.carry",
|
||||
"stations.list",
|
||||
"stations.sections",
|
||||
}
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
|
||||
@@ -69,8 +69,8 @@ def test_실행기_값_server_calc_와_같음() -> None:
|
||||
|
||||
|
||||
def test_Node_실행기_아닌_id_는_ValueError() -> None:
|
||||
with pytest.raises(ValueError, match="stations.list"):
|
||||
runner.run_calc(["masshaul.cumulative", "stations.list"], {})
|
||||
with pytest.raises(ValueError, match="ground.widen"):
|
||||
runner.run_calc(["masshaul.cumulative", "ground.widen"], {})
|
||||
|
||||
|
||||
# ── 472-4 ② — 횡단 설계선 · 소단 구간 Node 길 ──────────────────────────────────
|
||||
|
||||
@@ -114,8 +114,8 @@ def test_본문_상한_꼴_모르는_id(client, monkeypatch):
|
||||
assert http.post(url, content=b"{nope").status_code == 400
|
||||
assert http.post(url, json={"run": "cross.fit"}).status_code == 400
|
||||
assert http.post(url, json={"run": ["cross.fit"], "inputs": []}).status_code == 400
|
||||
bad = http.post(url, json={"run": ["stations.list"], "inputs": {}})
|
||||
assert bad.status_code == 400 and "stations.list" in bad.json()["message"]
|
||||
bad = http.post(url, json={"run": ["ground.widen"], "inputs": {}})
|
||||
assert bad.status_code == 400 and "ground.widen" in bad.json()["message"]
|
||||
|
||||
|
||||
def test_떠_있는_Node_답_파일_길과_같음_죽어도_다시():
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
"""PLAN 511-B B-2 (요구 320-4) — 서버 측점 생성 = 화면과 같은 TS(Node `stations.sections`).
|
||||
|
||||
못박는 것
|
||||
- 고정 자료(`fixtures/golden_472`) 노선 · 관 · 구조물 · 추가 측점 — `build_sections`
|
||||
Node 길 = 파이썬 `generate_sections` 길 0 차(칸 · 차례 · 값 · 측점 열쇠 `key` 만 더함) —
|
||||
곡선표 있음 · 없음 · 스냅 10 m · 측점 간격 10 m · 끝 측점 뺌.
|
||||
- DTM 아닌 지표면 = 파이썬 길(Node 안 부름 · 열쇠 없음).
|
||||
- 열쇠 = Node `stations.list`(화면 묶음 stations 단계 `buildStations`) 열쇠.
|
||||
- 창구(`/calc`)는 `stations.sections`(서버 파일 경로 입력)를 안 받음.
|
||||
- 등록부 R5 · R6 · R7 = node:stations.list · G4 = node:stations.sections(472-4 until 없음).
|
||||
- 공유 DB · 실제 storage 안 씀(고정 자료 사본).
|
||||
|
||||
./venv/Scripts/python.exe -m pytest resources/tester/test_511_b2_stations_node.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
for path in (ROOT, HERE):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import helper_472_1_paths as paths # noqa: E402
|
||||
|
||||
from B05_Profile import B05_Profile_Engine_Sections as sections # noqa: E402
|
||||
from B05_Profile import B05_Profile_Engine_Sections_Node as node # noqa: E402
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions # noqa: E402
|
||||
from common_util import common_util_calc_runner as runner # noqa: E402
|
||||
from common_util.common_util_surface_sampler import ( # noqa: E402
|
||||
CallableSurfaceSampler,
|
||||
build_surface_sampler,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (ROOT / "config" / "node_modules" / "vite").is_dir(), reason="프론트엔드 의존성 미설치"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def project(tmp_path_factory):
|
||||
root = paths.stage_project(tmp_path_factory.mktemp("b2") / "project")
|
||||
summary = paths.read("summary.json")["longitudinal"]["data"]
|
||||
options = SectionGenerationOptions(
|
||||
**{k: v for k, v in summary["options"].items() if k != "extra_stations"}
|
||||
)
|
||||
polyline = sections._load_route_polyline(root, paths.ROUTE)
|
||||
pipes = sections._load_pipe_points(root, polyline)
|
||||
facilities = sections.station_facilities(root, pipes, polyline)
|
||||
extras = sections.resolve_extra_stations(
|
||||
root, facilities[0], structures=facilities[1], plus=facilities[2]
|
||||
)
|
||||
return {
|
||||
"root": root,
|
||||
"options": options,
|
||||
"polyline": polyline,
|
||||
"pipes": pipes,
|
||||
"facilities": facilities,
|
||||
"extras": extras,
|
||||
"sampler": build_surface_sampler(root / "B04_PreProcess/models", *paths.SURFACE),
|
||||
"curves": sections._load_design_curves(root),
|
||||
}
|
||||
|
||||
|
||||
def _both(project, sampler=None, **change):
|
||||
kw = {
|
||||
"options": project["options"],
|
||||
"grade_options": None,
|
||||
"source_snapshot": {"filter": "classification"},
|
||||
"crs": "EPSG:5186",
|
||||
"design_curves": project["curves"],
|
||||
"station_snap": sections.load_station_snap(project["root"]),
|
||||
"station_snap_step": sections.load_station_snap_step(project["root"]),
|
||||
**change,
|
||||
}
|
||||
args = (project["polyline"], sampler or project["sampler"], project["pipes"], project["extras"])
|
||||
python, _ = sections.build_sections(*args, **kw)
|
||||
noded, _ = sections.build_sections(*args, facilities=project["facilities"], **kw)
|
||||
return python, noded
|
||||
|
||||
|
||||
def _without_key(value):
|
||||
if isinstance(value, dict):
|
||||
return {k: _without_key(v) for k, v in value.items() if k != "key"}
|
||||
if isinstance(value, list):
|
||||
return [_without_key(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _first_diff(a, b, path=""):
|
||||
"""첫 다른 자리(값 · 칸 차례) — 큰 글 비교를 pytest 에 안 넘김. 수 = 같은 값(0 ↔ 0.0)."""
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
if list(a) != list(b):
|
||||
return f"{path}: 칸 {list(a)} != {list(b)}"
|
||||
return next((d for k in a if (d := _first_diff(a[k], b[k], f"{path}.{k}"))), None)
|
||||
if isinstance(a, list) and isinstance(b, list):
|
||||
if len(a) != len(b):
|
||||
return f"{path}: 길이 {len(a)} != {len(b)}"
|
||||
return next(
|
||||
(d for i, (x, y) in enumerate(zip(a, b)) if (d := _first_diff(x, y, f"{path}[{i}]"))),
|
||||
None,
|
||||
)
|
||||
return None if a == b else f"{path}: {a!r} != {b!r}"
|
||||
|
||||
|
||||
def _same(python, noded):
|
||||
hit = _first_diff(python, _without_key(noded)) # 칸 차례 · 자릿수까지
|
||||
assert hit is None, hit
|
||||
keys = [s.get("key") for s in noded["longitudinal"]["stations"]]
|
||||
assert all(keys) and keys == [s["key"] for s in noded["cross_sections"]]
|
||||
return keys
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"case",
|
||||
["as_saved", "no_curves", "snap10", "interval10", "no_endpoint"],
|
||||
)
|
||||
def test_Node_길_파이썬_길_0차(project, case):
|
||||
change = {
|
||||
"as_saved": {},
|
||||
"no_curves": {"design_curves": []},
|
||||
"snap10": {"station_snap": True, "station_snap_step": 10.0},
|
||||
"interval10": {"options": replace(project["options"], station_interval_m=10.0)},
|
||||
"no_endpoint": {"options": replace(project["options"], include_endpoint=False)},
|
||||
}[case]
|
||||
python, noded = _both(project, **change)
|
||||
keys = _same(python, noded)
|
||||
assert len(keys) == len(set(keys)) > 10
|
||||
if case == "as_saved":
|
||||
assert len(keys) == 57 and keys[0] == "end:BP" and keys[-1] == "end:EP"
|
||||
kinds = {s["kind"] for s in python["longitudinal"]["stations"]}
|
||||
assert {"bp", "ep", "regular", "irregular"} <= kinds
|
||||
assert any(k.startswith("pipe:") for k in keys)
|
||||
|
||||
|
||||
def test_열쇠_Node_측점_목록과_같음(project):
|
||||
_, noded = _both(project)
|
||||
opts = project["options"]
|
||||
stations = node.stations_input(
|
||||
[[p[0], p[1]] for p in project["polyline"]],
|
||||
*project["facilities"],
|
||||
opts,
|
||||
project["curves"],
|
||||
sections.load_station_snap(project["root"]),
|
||||
sections.load_station_snap_step(project["root"]),
|
||||
)
|
||||
listed = node.node_stations(stations)
|
||||
assert [s["key"] for s in listed] == [s["key"] for s in noded["longitudinal"]["stations"]]
|
||||
assert [s["chainageM"] for s in listed] == [
|
||||
s["chainage_m"] for s in noded["longitudinal"]["stations"]
|
||||
]
|
||||
|
||||
|
||||
def test_DTM_아닌_지표면_파이썬_길(project, monkeypatch):
|
||||
called = []
|
||||
monkeypatch.setattr(runner, "run_calc_served", lambda *a: called.append(a))
|
||||
flat = CallableSurfaceSampler(lambda xy: np.full(len(xy), 100.0))
|
||||
python, noded = _both(project, sampler=flat)
|
||||
assert called == [] and python == noded
|
||||
assert "key" not in noded["longitudinal"]["stations"][0]
|
||||
|
||||
|
||||
def test_Node_못_돌림_파이썬_길(project, monkeypatch):
|
||||
monkeypatch.setattr(runner, "run_calc_served", lambda *a: None)
|
||||
python, noded = _both(project)
|
||||
assert python == noded
|
||||
|
||||
|
||||
def test_창구는_서버_측점_생성_안_받음():
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from B06_Section import B06_Section_Router_Calc as calc_router
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(calc_router.router)
|
||||
http = TestClient(app)
|
||||
url = "/api/projects/11111111-2222-3333-4444-555555555555/calc"
|
||||
body = {"run": ["stations.sections"], "inputs": {"stations": {"grid": "C:/x"}}}
|
||||
bad = http.post(url, json=body)
|
||||
assert bad.status_code == 400 and "창구 밖" in bad.json()["message"]
|
||||
assert runner.SERVER_ONLY_IDS <= runner.NODE_IDS
|
||||
|
||||
|
||||
def test_등록부_측점_줄_Node():
|
||||
src = (ROOT / "A00_Common/design/logic/design_calc_registry.ts").read_text(encoding="utf-8")
|
||||
for no, run in (
|
||||
("R5", "stations.list"),
|
||||
("R6", "stations.list"),
|
||||
("R7", "stations.list"),
|
||||
("G4", "stations.sections"),
|
||||
):
|
||||
row = src.split(f'no: "{no}"')[1].split("\n },")[0]
|
||||
assert f'runner: "node:{run}"' in row and "until" not in row, no
|
||||
Reference in New Issue
Block a user