Merge remote-tracking branches 'origin/sub_desktop_1' and 'origin/sub_laptop_1' into main_laptop_1
This commit is contained in:
@@ -37,7 +37,11 @@ from common_util.common_util_drainage_pipes import (
|
||||
route_signature,
|
||||
)
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from common_util.common_util_route_geometry import RouteVertex
|
||||
from common_util.common_util_route_geometry import (
|
||||
RouteVertex,
|
||||
planned_route_initial_path,
|
||||
planned_route_working_path,
|
||||
)
|
||||
from common_util.common_util_surface_sampler import build_surface_sampler
|
||||
from config.config_system import (
|
||||
DRAINAGE_CACHE_DIRNAME,
|
||||
@@ -66,6 +70,34 @@ def _load_route_polyline(project_root: Path, route_data_path: str) -> list[list[
|
||||
return [[float(c[0]), float(c[1]), float(c[2]) if len(c) > 2 else 0.0] for c in coords]
|
||||
|
||||
|
||||
def _load_design_curves(project_root: Path) -> list[dict[str, Any]]:
|
||||
"""설계가 쓰는 계획노선의 **곡선표** — 없으면 빈 목록.
|
||||
|
||||
곡선표는 폴리라인 파일 옆에 같은 이름으로 선다(`…_curves.json`). 정점 목록만으로는
|
||||
어디부터 어디까지가 한 곡선이고 반지름이 얼마인지 알 수 없어, 곡선부 확폭을 측점마다
|
||||
반경을 다시 재는 방식으로 매기면 같은 곡선 안에서도 값이 갈린다(2026-09-12 실측).
|
||||
|
||||
수정본(`planned_route.csv`)이 있으면 **그 곡선표만** 쓴다 — 노선을 고쳤는데 초기본
|
||||
곡선표를 읽으면 있지도 않은 자리에 확폭이 붙는다. 곡선표가 없으면 빈 목록을 돌려주고,
|
||||
받는 쪽이 옛 방식(측점별 실측 반경)으로 물러선다.
|
||||
"""
|
||||
route_path = planned_route_working_path(project_root)
|
||||
if not route_path.is_file():
|
||||
route_path = planned_route_initial_path(project_root)
|
||||
target = route_path.with_name(f"{route_path.stem}_curves.json")
|
||||
if not target.is_file():
|
||||
return []
|
||||
try:
|
||||
data = json.loads(target.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
logger.warning("계획노선 곡선표를 읽지 못했습니다: %s", target)
|
||||
return []
|
||||
curves = data.get("curves") if isinstance(data, dict) else None
|
||||
return (
|
||||
[curve for curve in curves if isinstance(curve, dict)] if isinstance(curves, list) else []
|
||||
)
|
||||
|
||||
|
||||
def cross_filename(chainage_m: float) -> str:
|
||||
"""측점 chainage에 대응하는 횡단면 파일명(단일 규칙)."""
|
||||
return f"cross_{int(round(float(chainage_m))):05d}m.json"
|
||||
@@ -361,6 +393,9 @@ def run_section_generation(
|
||||
replace(options or SectionGenerationOptions(), extra_stations=extras),
|
||||
source_snapshot={"filter": filter_key, "method": method, "smooth": smooth},
|
||||
crs=crs,
|
||||
# 곡선부 확폭의 정본 — 설계 곡선표(시·종점·반지름)를 그대로 쓴다. 없으면 빈 목록이라
|
||||
# 종전의 측점별 실측 반경으로 물러선다.
|
||||
design_curves=_load_design_curves(project_root),
|
||||
)
|
||||
|
||||
stage_root = project_root / _STAGE_SUBDIR
|
||||
@@ -473,6 +508,8 @@ def generate_irregular_sections(
|
||||
merged_options,
|
||||
source_snapshot={"filter": filter_key, "method": method, "smooth": smooth},
|
||||
crs=crs,
|
||||
# 비정규(구조물) 측점도 같은 곡선표를 봐야 규칙 측점과 확폭이 어긋나지 않는다.
|
||||
design_curves=_load_design_curves(project_root),
|
||||
)
|
||||
irregular_stations = [
|
||||
station
|
||||
|
||||
@@ -213,6 +213,101 @@ def _curve_widenings(
|
||||
return widenings, sides
|
||||
|
||||
|
||||
#: 곡선표의 시·종점이 노선 폴리라인에서 이만큼 떨어져 있으면 그 노선의 곡선이 아니라고 본다(m).
|
||||
#: 노선을 잘라 쓰면(지표면 밖 트림) 곡선표에 남은 옛 곡선이 노선 밖에 뜬다.
|
||||
DESIGN_CURVE_MATCH_TOLERANCE_M = 5.0
|
||||
|
||||
|
||||
def _project_chainage(
|
||||
points: np.ndarray, route_chainage: np.ndarray, xy: tuple[float, float]
|
||||
) -> tuple[float, float]:
|
||||
"""점을 노선 폴리라인 위로 내려 (떨어진 거리 m, 누가거리 m)."""
|
||||
starts = points[:-1, :2]
|
||||
vectors = points[1:, :2] - starts
|
||||
lengths2 = np.einsum("ij,ij->i", vectors, vectors)
|
||||
safe = np.where(lengths2 > 1e-12, lengths2, 1.0)
|
||||
target = np.asarray(xy, dtype=np.float64)
|
||||
ratios = np.clip(np.einsum("ij,ij->i", target - starts, vectors) / safe, 0.0, 1.0)
|
||||
feet = starts + vectors * ratios[:, None]
|
||||
distances = np.hypot(feet[:, 0] - target[0], feet[:, 1] - target[1])
|
||||
index = int(np.argmin(distances))
|
||||
span = route_chainage[index + 1] - route_chainage[index]
|
||||
return float(distances[index]), float(route_chainage[index] + span * ratios[index])
|
||||
|
||||
|
||||
def _design_curve_spans(
|
||||
points: np.ndarray,
|
||||
route_chainage: np.ndarray,
|
||||
curves: list[dict[str, Any]],
|
||||
) -> list[tuple[float, float, float, str]]:
|
||||
"""설계 곡선표 → [(시점 누가거리, 종점 누가거리, 반지름 m, 곡선 **바깥쪽**)].
|
||||
|
||||
바깥쪽은 회전 방향으로 가른다 — 시점→교점→종점의 외적 z가 양수면 좌회전이라 안쪽이
|
||||
좌측이고 바깥은 우측이다(`_plan_radii` 와 같은 규약). 노선에서 멀리 떨어진 곡선과
|
||||
방향을 못 재는 곡선은 버린다.
|
||||
"""
|
||||
spans: list[tuple[float, float, float, str]] = []
|
||||
for curve in curves:
|
||||
start, apex, end = curve.get("start"), curve.get("apex"), curve.get("end")
|
||||
radius = curve.get("radius_m")
|
||||
if not (start and apex and end) or not isinstance(radius, (int, float)):
|
||||
continue
|
||||
start_gap, start_chainage = _project_chainage(points, route_chainage, tuple(start[:2]))
|
||||
end_gap, end_chainage = _project_chainage(points, route_chainage, tuple(end[:2]))
|
||||
if max(start_gap, end_gap) > DESIGN_CURVE_MATCH_TOLERANCE_M:
|
||||
continue
|
||||
cross = (apex[0] - start[0]) * (end[1] - apex[1]) - (apex[1] - start[1]) * (
|
||||
end[0] - apex[0]
|
||||
)
|
||||
if abs(cross) < 1e-12:
|
||||
continue
|
||||
spans.append(
|
||||
(
|
||||
min(start_chainage, end_chainage),
|
||||
max(start_chainage, end_chainage),
|
||||
float(radius),
|
||||
"right" if cross > 0 else "left",
|
||||
)
|
||||
)
|
||||
return sorted(spans)
|
||||
|
||||
|
||||
def _design_curve_widenings(
|
||||
station_chainage: np.ndarray,
|
||||
spans: list[tuple[float, float, float, str]],
|
||||
) -> tuple[list[float | None], list[str | None], list[float]]:
|
||||
"""설계 곡선표로 측점별 (평면 곡선반경, 바깥쪽, 확폭량)을 낸다.
|
||||
|
||||
곡선 **안**은 그 곡선의 설계 반경이 그대로 반경이고 확폭도 표값 한 값이다. 곡선 앞뒤
|
||||
`CURVE_WIDENING_TAPER_M` 구간은 0 으로 잇고(실무 관행 직선 테이퍼), 그 밖은 확폭이
|
||||
없다. 곡선이 겹치면 **확폭이 큰 쪽**을 따르고, 반경은 작은 쪽(급한 쪽)을 남긴다.
|
||||
"""
|
||||
count = len(station_chainage)
|
||||
radii: list[float | None] = [None] * count
|
||||
sides: list[str | None] = [None] * count
|
||||
widenings: list[float] = [0.0] * count
|
||||
for start, end, radius, side in spans:
|
||||
table = curve_widening_m(radius)
|
||||
for index, value in enumerate(station_chainage):
|
||||
chainage = float(value)
|
||||
inside = start - 1e-9 <= chainage <= end + 1e-9
|
||||
if inside and (radii[index] is None or radius < radii[index]):
|
||||
radii[index] = round(radius, 3)
|
||||
if table <= 0.0:
|
||||
continue
|
||||
if inside:
|
||||
amount = table
|
||||
else:
|
||||
distance = start - chainage if chainage < start else chainage - end
|
||||
if distance > CURVE_WIDENING_TAPER_M:
|
||||
continue
|
||||
amount = round(table * (1.0 - distance / CURVE_WIDENING_TAPER_M), 4)
|
||||
if amount > widenings[index]:
|
||||
widenings[index] = amount
|
||||
sides[index] = side
|
||||
return radii, sides, widenings
|
||||
|
||||
|
||||
def generate_sections(
|
||||
polyline: np.ndarray | list[list[float]],
|
||||
sampler: SurfaceElevationSampler,
|
||||
@@ -220,6 +315,7 @@ def generate_sections(
|
||||
*,
|
||||
source_snapshot: dict[str, Any] | None = None,
|
||||
crs: str | None = None,
|
||||
design_curves: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""확정 경로로 CAD 인계 가능한 종단·횡단 원시 데이터를 생성한다."""
|
||||
options = options or SectionGenerationOptions()
|
||||
@@ -267,11 +363,21 @@ def generate_sections(
|
||||
# 측점별 **평면 곡선반경**(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))과 최소곡선반지름 위반
|
||||
# 표시가 이 값을 쓴다(2026-09-06). 노선 폴리라인 위에서 앞뒤로 같은 거리를 떨어진 세
|
||||
# 점의 외접원 반경이며, 직선이면 무한대라 None 으로 낸다.
|
||||
plan_radii, plan_outer_sides = _plan_radii(points, route_chainage, station_chainage, total)
|
||||
# 확폭량은 여기서 한 번에 낸다 — 테이퍼가 이웃 측점을 봐야 하므로 측점 단위로는 못 낸다.
|
||||
plan_widenings, plan_outer_sides = _curve_widenings(
|
||||
station_chainage, plan_radii, plan_outer_sides
|
||||
)
|
||||
# **설계 곡선표가 있으면 그것이 정본**이다(2026-09-12 사용자 지시). 측점마다 반경을
|
||||
# 다시 재면 같은 곡선 안에서도 확폭이 갈리고, 곡선이 측점 간격보다 짧으면 통째로
|
||||
# 빠진다(실측: 설계 12m 곡선에 1.50m 이 붙고 곡선 밖 직선까지 흘러나갔다).
|
||||
# 곡선표가 없는 옛 프로젝트만 종전의 측점별 실측으로 물러선다.
|
||||
spans = _design_curve_spans(points, route_chainage, design_curves or [])
|
||||
if spans:
|
||||
plan_radii, plan_outer_sides, plan_widenings = _design_curve_widenings(
|
||||
station_chainage, spans
|
||||
)
|
||||
else:
|
||||
plan_radii, plan_outer_sides = _plan_radii(points, route_chainage, station_chainage, total)
|
||||
# 확폭량은 여기서 한 번에 낸다 — 테이퍼가 이웃 측점을 봐야 하므로 측점 단위로는 못 낸다.
|
||||
plan_widenings, plan_outer_sides = _curve_widenings(
|
||||
station_chainage, plan_radii, plan_outer_sides
|
||||
)
|
||||
|
||||
offsets = np.arange(
|
||||
-options.cross_half_width_m,
|
||||
|
||||
@@ -721,32 +721,55 @@ export function appendCrossDesignOverlay(
|
||||
}
|
||||
|
||||
// 차도·노견 경계 짧은 수직 틱(N-4-2): ±3.6px(기존 ±6의 60%). 노면 단일 기울기라 육안
|
||||
// 구분이 안 되는 경계를 표시한다. 노견 바깥 끝(road_edges)은 측구·사면 꺾임으로 이미 구분됨.
|
||||
// 구분이 안 되는 경계를 표시한다.
|
||||
const edges = design.carriageway_edges;
|
||||
if (edges) {
|
||||
for (const edge of [edges.left, edges.right]) {
|
||||
const cx = x(edge.offset_m);
|
||||
const cy = toDisplayY(edge.elevation_m);
|
||||
const tickAt = (offsetM: number, elevationM: number, className: string, half: number) => {
|
||||
const cx = x(offsetM);
|
||||
const cy = toDisplayY(elevationM);
|
||||
const tick = document.createElementNS(SVG_NS, "line");
|
||||
tick.setAttribute("x1", String(cx));
|
||||
tick.setAttribute("y1", String(cy - 3.6));
|
||||
tick.setAttribute("y1", String(cy - half));
|
||||
tick.setAttribute("x2", String(cx));
|
||||
tick.setAttribute("y2", String(cy + 3.6));
|
||||
tick.setAttribute("class", "b06-chart__carriageway-tick");
|
||||
tick.setAttribute("y2", String(cy + half));
|
||||
tick.setAttribute("class", className);
|
||||
svg.append(tick);
|
||||
};
|
||||
for (const edge of [edges.left, edges.right]) {
|
||||
tickAt(edge.offset_m, edge.elevation_m, "b06-chart__carriageway-tick", 3.6);
|
||||
}
|
||||
// 노견 바깥 끝에도 틱을 세운다(2026-09-12 사용자) — 종전에는 차도에만 표기가 있어
|
||||
// **노견이 늘어난 건지 차도가 늘어난 건지 화면에서 못 가렸다**. 노견은 좌·우 0.5m 로
|
||||
// 고정이고 확폭은 차도에만 붙으므로, 두 틱 사이 간격이 그 사실을 그대로 보여 준다.
|
||||
const roadEdges = design.road_edges;
|
||||
if (roadEdges) {
|
||||
for (const edge of [roadEdges.left, roadEdges.right]) {
|
||||
tickAt(edge.offset_m, edge.elevation_m, "b06-chart__shoulder-tick", 2.4);
|
||||
}
|
||||
}
|
||||
// 노폭 라벨(2026-09-06 사용자 지시) — 확폭이 걸린 측점인지 눈으로 바로 알게 한다.
|
||||
// 확폭이 없으면 규격 폭만, 있으면 「4.5m (규격 3.0 + 확폭 1.5)」로 적는다.
|
||||
const widened = (design.widening_left_m ?? 0) + (design.widening_right_m ?? 0);
|
||||
const standardWidth = design.carriageway_standard_width_m;
|
||||
const shoulderLeft = roadEdges ? roadEdges.left.offset_m - edges.left.offset_m : null;
|
||||
const shoulderRight = roadEdges ? edges.right.offset_m - roadEdges.right.offset_m : null;
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
label.setAttribute("x", String((x(edges.left.offset_m) + x(edges.right.offset_m)) / 2));
|
||||
label.setAttribute("y", String(toDisplayY(edges.left.elevation_m) - 6));
|
||||
label.setAttribute("class", "b06-chart__carriageway-label");
|
||||
label.textContent =
|
||||
const widthText =
|
||||
widened > 0.001 && typeof standardWidth === "number"
|
||||
? `노폭 ${design.carriageway_width_m.toFixed(2)}m (규격 ${standardWidth.toFixed(2)} + 확폭 ${widened.toFixed(2)})`
|
||||
: `노폭 ${design.carriageway_width_m.toFixed(2)}m`;
|
||||
// 노견은 좌우가 같으면 한 번만 적는다 — 라벨이 길어지면 옆 측점 라벨과 겹친다.
|
||||
label.textContent =
|
||||
shoulderLeft != null && shoulderRight != null
|
||||
? `${widthText} · 노견 ${
|
||||
Math.abs(shoulderLeft - shoulderRight) < 0.005
|
||||
? `${shoulderLeft.toFixed(2)}m`
|
||||
: `좌 ${shoulderLeft.toFixed(2)} / 우 ${shoulderRight.toFixed(2)}m`
|
||||
}`
|
||||
: widthText;
|
||||
svg.append(label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,14 @@
|
||||
stroke-width: 1.2;
|
||||
}
|
||||
|
||||
/* 노견 바깥 끝 틱(2026-09-12) — 차도 틱보다 짧고 옅다. 차도 틱과 이 틱 사이가 노견이라,
|
||||
노면이 넓어졌을 때 차도가 늘었는지 노견이 늘었는지 눈으로 바로 갈린다. */
|
||||
.b06-chart__shoulder-tick {
|
||||
stroke: var(--color-royal-amethyst);
|
||||
stroke-width: 1;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
/* 노폭 라벨(2026-09-06) — 차도 위 가운데. 확폭이 걸린 측점을 눈으로 가려내는 표기다. */
|
||||
.b06-chart__carriageway-label {
|
||||
fill: var(--color-royal-amethyst);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
|
||||
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import {
|
||||
drawBaseDataTab,
|
||||
drawFactorChoices,
|
||||
@@ -175,6 +176,14 @@ function injectStyles(): void {
|
||||
style.textContent = `
|
||||
.b09-panel { display: flex; flex-direction: column; gap: var(--space-md, 12px); }
|
||||
.b09-panel__group { display: flex; flex-direction: column; gap: var(--space-xs, 4px); }
|
||||
/* 좌측 패널 상자 — B04~B07 과 같은 꼴(테두리는 공용 .ui-sidebar-section 이 전담).
|
||||
.ui-sidebar-section 이 붙은 것만 집어 본문 기초자료 표의 같은 클래스는 안 건드린다. */
|
||||
.b09-panel__group.ui-sidebar-section {
|
||||
margin: 0;
|
||||
padding: calc(var(--spacing-8) + var(--spacing-4));
|
||||
border-radius: var(--radius-cards);
|
||||
background-color: var(--color-surface-raised);
|
||||
}
|
||||
.b09-panel__legend {
|
||||
font-size: var(--font-size-xs, 12px); letter-spacing: .06em;
|
||||
color: var(--color-text-secondary); text-transform: uppercase;
|
||||
@@ -184,7 +193,6 @@ function injectStyles(): void {
|
||||
display: flex; justify-content: space-between; gap: var(--space-sm, 8px);
|
||||
border-bottom: 1px solid var(--color-border); padding: 2px 0;
|
||||
}
|
||||
.b09-panel__actions { display: flex; gap: var(--space-xs, 4px); margin-top: var(--space-sm, 8px); }
|
||||
.b09-hint { font-size: var(--font-size-xs, 12px); color: var(--color-text-secondary); }
|
||||
/* 표본이 얇은 노임 — 막는 것이 아니라 눈에 띄기만 하면 된다. */
|
||||
.b09-hint--warn { color: var(--color-warning-text, #8a5a00); }
|
||||
@@ -483,10 +491,10 @@ function buildSidePanel(
|
||||
legendKey: keyof typeof ui_locales,
|
||||
fields: Array<[keyof CostFormState, keyof typeof ui_locales]>,
|
||||
): void => {
|
||||
const group = document.createElement("div");
|
||||
group.className = "b09-panel__group";
|
||||
const group = document.createElement("section");
|
||||
group.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const legend = document.createElement("span");
|
||||
legend.className = "b09-panel__legend";
|
||||
legend.className = "b09-panel__legend ui-collapsible__title";
|
||||
legend.textContent = L(legendKey);
|
||||
group.append(legend);
|
||||
for (const [field, labelKey] of fields) {
|
||||
@@ -512,10 +520,10 @@ function buildSidePanel(
|
||||
]);
|
||||
|
||||
// 요율 판 — 읽기 전용. 「어느 판으로 계산했나」가 화면에 남아야 재현성이 선다.
|
||||
const rateGroup = document.createElement("div");
|
||||
rateGroup.className = "b09-panel__group";
|
||||
const rateGroup = document.createElement("section");
|
||||
rateGroup.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const rateLegend = document.createElement("span");
|
||||
rateLegend.className = "b09-panel__legend";
|
||||
rateLegend.className = "b09-panel__legend ui-collapsible__title";
|
||||
rateLegend.textContent = L("B09_Estimation_Group_RateVersion");
|
||||
const rateVersionBox = document.createElement("div");
|
||||
rateGroup.append(rateLegend, rateVersionBox);
|
||||
@@ -532,10 +540,10 @@ function buildSidePanel(
|
||||
]);
|
||||
|
||||
// 수량 — 여러 줄이라 텍스트 영역으로. 비어 있으면 위 직접비 3칸을 그대로 쓴다.
|
||||
const quantityGroup = document.createElement("div");
|
||||
quantityGroup.className = "b09-panel__group";
|
||||
const quantityGroup = document.createElement("section");
|
||||
quantityGroup.className = "b09-panel__group ui-collapsible ui-sidebar-section";
|
||||
const quantityLegend = document.createElement("span");
|
||||
quantityLegend.className = "b09-panel__legend";
|
||||
quantityLegend.className = "b09-panel__legend ui-collapsible__title";
|
||||
quantityLegend.textContent = L("B09_Estimation_Group_Quantity");
|
||||
const quantityLabel = document.createElement("label");
|
||||
quantityLabel.className = "ui-field__label";
|
||||
@@ -555,7 +563,8 @@ function buildSidePanel(
|
||||
root.append(hintBox);
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "b09-panel__actions";
|
||||
// 바닥 고정 액션 줄(공용) — ui_template_overlay 가 이 줄을 스크롤 밖으로 빼낸다.
|
||||
actions.className = "ui-sidebar-actions";
|
||||
actions.append(
|
||||
createButton({
|
||||
label: L("B09_Estimation_Btn_Recalc"),
|
||||
@@ -569,6 +578,9 @@ function buildSidePanel(
|
||||
);
|
||||
root.append(actions);
|
||||
|
||||
// 그룹 제목 행 클릭 시 접기/펼치기(B04~B07 공통). 액션 줄은 collapsible 이 아니다.
|
||||
attachCollapsible(root);
|
||||
|
||||
return { root, rateVersionBox, hintBox };
|
||||
}
|
||||
|
||||
|
||||
@@ -148,3 +148,64 @@ def test_widening_without_curve_stays_zero() -> None:
|
||||
widenings, sides = _curve_widenings(chainage, [None] * 3, [None] * 3)
|
||||
assert widenings == [0.0, 0.0, 0.0]
|
||||
assert sides == [None, None, None]
|
||||
|
||||
|
||||
def test_design_curve_spans_from_curve_table() -> None:
|
||||
"""설계 곡선표의 시·종점이 노선 누가거리로 바뀌고, 회전 방향으로 바깥쪽이 갈린다."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_spans
|
||||
|
||||
# ㄱ자 노선 — (0,0) → (100,0) → (100,100). 교점 (100,0) 에서 좌회전.
|
||||
line = np.array([[0.0, 0.0, 0.0], [100.0, 0.0, 0.0], [100.0, 100.0, 0.0]])
|
||||
chainage = np.array([0.0, 100.0, 200.0])
|
||||
curves = [
|
||||
{
|
||||
"start": [90.0, 0.0],
|
||||
"apex": [100.0, 0.0],
|
||||
"end": [100.0, 10.0],
|
||||
"radius_m": 12.0,
|
||||
}
|
||||
]
|
||||
spans = _design_curve_spans(line, chainage, curves)
|
||||
assert len(spans) == 1
|
||||
start, end, radius, side = spans[0]
|
||||
assert abs(start - 90.0) < 1e-6 and abs(end - 110.0) < 1e-6
|
||||
assert radius == 12.0
|
||||
# 좌회전이면 안쪽이 좌측이라 바깥은 우측이다.
|
||||
assert side == "right"
|
||||
# 노선에서 멀리 떨어진 곡선은 버린다.
|
||||
assert _design_curve_spans(line, chainage, [{**curves[0], "start": [90.0, 500.0]}]) == []
|
||||
|
||||
|
||||
def test_design_curve_widening_is_one_value_inside_the_curve() -> None:
|
||||
"""같은 곡선 안 측점은 설계 반경의 표값 하나를 쓰고, 앞뒤 10m 만 이어 준다."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_widenings
|
||||
|
||||
# 곡선 100~140m, 설계 반경 16m(표값 1.5m). 측점 5m 간격.
|
||||
spans = [(100.0, 140.0, 16.0, "left")]
|
||||
chainage = np.arange(80.0, 165.0, 5.0)
|
||||
radii, sides, widenings = _design_curve_widenings(chainage, spans)
|
||||
table = dict(zip(chainage.tolist(), widenings, strict=True))
|
||||
# 곡선 안은 어디서나 같은 값 — 종전에는 측점마다 반경을 다시 재 값이 갈렸다.
|
||||
for station in (100.0, 110.0, 120.0, 130.0, 140.0):
|
||||
assert table[station] == 1.5, station
|
||||
# 앞뒤 10m 는 0 으로 잇는다.
|
||||
assert table[95.0] == 0.75 and table[145.0] == 0.75
|
||||
assert table[90.0] == 0.0 and table[150.0] == 0.0
|
||||
# 반경은 곡선 안에서만 남고, 테이퍼·직선 자리는 비어 있다.
|
||||
by_index = dict(zip(chainage.tolist(), radii, strict=True))
|
||||
assert by_index[120.0] == 16.0
|
||||
assert by_index[95.0] is None and by_index[90.0] is None
|
||||
# 방향은 곡선 것을 따라간다.
|
||||
assert sides[chainage.tolist().index(95.0)] == "left"
|
||||
|
||||
|
||||
def test_design_curve_widening_takes_the_wider_of_overlapping_curves() -> None:
|
||||
"""곡선이 겹치거나 붙어 있으면 확폭이 큰 쪽을 따른다."""
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import _design_curve_widenings
|
||||
|
||||
spans = [(100.0, 120.0, 35.0, "left"), (118.0, 140.0, 12.0, "right")]
|
||||
chainage = np.array([110.0, 119.0, 130.0])
|
||||
_, sides, widenings = _design_curve_widenings(chainage, spans)
|
||||
assert widenings[0] == 0.5 # R=35 → 0.5m
|
||||
assert widenings[1] == 2.25 and sides[1] == "right" # 겹친 자리는 급한 곡선 값
|
||||
assert widenings[2] == 2.25
|
||||
|
||||
Reference in New Issue
Block a user