feat(B05,B06): 측점별 평면 곡선반경 산출 + 법정 최소반경 경고, 자동탐색 입구 내림
2026-09-06 사용자 확정. 자동탐색은 평면만 보고 노선을 정해 실제 판단과 맞지 않으므로 쓰지 않는다 — 버튼만 감추고 코드·API 는 남긴다. 대신 법정 평면 기준을 **경고**로 낸다. - 횡단 생성이 측점마다 평면 곡선반경(`plan_radius_m`)을 낸다. 노선 폴리라인 위에서 앞뒤 10m 떨어진 세 점의 외접원 반경이며, 직선(1만m 이상)은 null. 반지름 50m 원호로 50.008 이 나오는 것을 확인. - 법정 최소곡선반지름 표(별표2 Ⅰ.2.다.(1) — 40:60/40, 30:30/20, 20:15/12)와 배향곡선 하한 10m 를 설정에 넣고 계획선 정책에 실어 화면으로 내린다. 탐색이 쓰는 등급별 상수와는 별개다. - 종단 상단줄에 「곡선반경 부족 n곳 / 최소 R」 경고 추가 — 자동 보정·차단은 없다. 툴팁에 하한값과 배향곡선 하한 미만 개수를 적는다. - 이 반경은 다음 작업(곡선부 확폭)이 그대로 쓴다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,18 @@ def legal_grade_limit_pct(
|
|||||||
return legal_max
|
return legal_max
|
||||||
|
|
||||||
|
|
||||||
|
def legal_plan_radius_min_m(design_speed_kph: int, terrain_type: str = "normal") -> float:
|
||||||
|
"""설계속도 × 지형 구분에 따른 법정 **평면** 최소곡선반지름(m, 별표2 Ⅰ.2.다.(1)).
|
||||||
|
|
||||||
|
경로탐색 제약이 아니라 **위반 표시** 기준이다(2026-09-06 사용자 확정). 설계속도는
|
||||||
|
이미 확정된 값을 받는다(계획선 옵션이 `resolve_design_speed`로 눌러 둔 값).
|
||||||
|
"""
|
||||||
|
table = FOREST_ROAD_PROFILE_CRITERIA["min_plan_radius_m"]
|
||||||
|
terrain = terrain_type if terrain_type in GRADE_TERRAIN_TYPES else "normal"
|
||||||
|
speeds = table.get(int(design_speed_kph)) or table[20]
|
||||||
|
return float(speeds[terrain])
|
||||||
|
|
||||||
|
|
||||||
def _pick(*candidates: Any) -> Any:
|
def _pick(*candidates: Any) -> Any:
|
||||||
"""요청 → DB 저장값 → config 순으로 처음 나오는 유효값을 고른다."""
|
"""요청 → DB 저장값 → config 순으로 처음 나오는 유효값을 고른다."""
|
||||||
for value in candidates:
|
for value in candidates:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from typing import Any
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT
|
from config.config_system import FOREST_ROAD_PROFILE_ALIGNMENT, FOREST_ROAD_PROFILE_CRITERIA
|
||||||
|
|
||||||
ALIGNMENT_SCHEMA_VERSION = 1
|
ALIGNMENT_SCHEMA_VERSION = 1
|
||||||
# chainage를 dict 키로 쓸 때의 표기. 프론트엔드(`toFixed(3)`)와 반드시 같아야 한다.
|
# chainage를 dict 키로 쓸 때의 표기. 프론트엔드(`toFixed(3)`)와 반드시 같아야 한다.
|
||||||
@@ -51,6 +51,10 @@ class AlignmentPolicy:
|
|||||||
max_grade_pct: float
|
max_grade_pct: float
|
||||||
curve_skip_delta_pct: float
|
curve_skip_delta_pct: float
|
||||||
paved: bool
|
paved: bool
|
||||||
|
#: 법정 평면 최소곡선반지름(m) — 위반 표시 기준(2026-09-06). 0이면 판정하지 않는다.
|
||||||
|
min_plan_radius_m: float = 0.0
|
||||||
|
#: 배향곡선 하한(m) — 이보다 급하면 경고만 낸다.
|
||||||
|
hairpin_min_radius_m: float = 0.0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_config(
|
def from_config(
|
||||||
@@ -60,6 +64,7 @@ class AlignmentPolicy:
|
|||||||
max_grade_pct: float,
|
max_grade_pct: float,
|
||||||
curve_skip_delta_pct: float,
|
curve_skip_delta_pct: float,
|
||||||
paved: bool,
|
paved: bool,
|
||||||
|
min_plan_radius_m: float = 0.0,
|
||||||
) -> "AlignmentPolicy":
|
) -> "AlignmentPolicy":
|
||||||
config = FOREST_ROAD_PROFILE_ALIGNMENT
|
config = FOREST_ROAD_PROFILE_ALIGNMENT
|
||||||
return cls(
|
return cls(
|
||||||
@@ -76,6 +81,8 @@ class AlignmentPolicy:
|
|||||||
max_grade_pct=float(max_grade_pct),
|
max_grade_pct=float(max_grade_pct),
|
||||||
curve_skip_delta_pct=float(curve_skip_delta_pct),
|
curve_skip_delta_pct=float(curve_skip_delta_pct),
|
||||||
paved=bool(paved),
|
paved=bool(paved),
|
||||||
|
min_plan_radius_m=float(min_plan_radius_m),
|
||||||
|
hairpin_min_radius_m=float(FOREST_ROAD_PROFILE_CRITERIA["hairpin_min_radius_m"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -140,6 +147,9 @@ class AlignmentPolicy:
|
|||||||
"max_grade_pct": self.max_grade_pct,
|
"max_grade_pct": self.max_grade_pct,
|
||||||
"curve_skip_delta_pct": self.curve_skip_delta_pct,
|
"curve_skip_delta_pct": self.curve_skip_delta_pct,
|
||||||
"paved": self.paved,
|
"paved": self.paved,
|
||||||
|
# 평면 곡선 판정값 — 화면 위반 표시가 쓴다(2026-09-06).
|
||||||
|
"min_plan_radius_m": self.min_plan_radius_m,
|
||||||
|
"hairpin_min_radius_m": self.hairpin_min_radius_m,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from B05_Profile.B05_Profile_Engine_Grade import (
|
|||||||
GradeDesignOptions,
|
GradeDesignOptions,
|
||||||
detect_main_direction,
|
detect_main_direction,
|
||||||
ground_profile,
|
ground_profile,
|
||||||
|
legal_plan_radius_min_m,
|
||||||
)
|
)
|
||||||
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
from B05_Profile.B05_Profile_Engine_Grade_Alignment import (
|
||||||
ALIGNMENT_SCHEMA_VERSION,
|
ALIGNMENT_SCHEMA_VERSION,
|
||||||
@@ -179,6 +180,8 @@ def design_ground_following_profile(
|
|||||||
max_grade_pct=options.max_grade_pct,
|
max_grade_pct=options.max_grade_pct,
|
||||||
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
||||||
paved=options.paved,
|
paved=options.paved,
|
||||||
|
# 평면 최소곡선반지름은 화면 위반 표시에만 쓴다(2026-09-06) — 탐색 제약과 별개.
|
||||||
|
min_plan_radius_m=legal_plan_radius_min_m(options.design_speed_kph, options.terrain_type),
|
||||||
)
|
)
|
||||||
|
|
||||||
warnings = list(options.warnings)
|
warnings = list(options.warnings)
|
||||||
@@ -270,6 +273,8 @@ def design_pipe_anchored_profile(
|
|||||||
max_grade_pct=options.max_grade_pct,
|
max_grade_pct=options.max_grade_pct,
|
||||||
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
||||||
paved=options.paved,
|
paved=options.paved,
|
||||||
|
# 평면 최소곡선반지름은 화면 위반 표시에만 쓴다(2026-09-06) — 탐색 제약과 별개.
|
||||||
|
min_plan_radius_m=legal_plan_radius_min_m(options.design_speed_kph, options.terrain_type),
|
||||||
)
|
)
|
||||||
|
|
||||||
warnings = list(options.warnings)
|
warnings = list(options.warnings)
|
||||||
@@ -385,6 +390,8 @@ def design_alignment_profile(
|
|||||||
max_grade_pct=options.max_grade_pct,
|
max_grade_pct=options.max_grade_pct,
|
||||||
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
curve_skip_delta_pct=options.vertical_curve_skip_delta_pct,
|
||||||
paved=options.paved,
|
paved=options.paved,
|
||||||
|
# 평면 최소곡선반지름은 화면 위반 표시에만 쓴다(2026-09-06) — 탐색 제약과 별개.
|
||||||
|
min_plan_radius_m=legal_plan_radius_min_m(options.design_speed_kph, options.terrain_type),
|
||||||
)
|
)
|
||||||
|
|
||||||
warnings = list(options.warnings)
|
warnings = list(options.warnings)
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ def _cross_summary(cross_section: dict[str, Any]) -> dict[str, Any]:
|
|||||||
"chainage_m": cross_section.get("chainage_m"),
|
"chainage_m": cross_section.get("chainage_m"),
|
||||||
"center_z": cross_section.get("center_z"),
|
"center_z": cross_section.get("center_z"),
|
||||||
"azimuth_deg": cross_section.get("azimuth_deg"),
|
"azimuth_deg": cross_section.get("azimuth_deg"),
|
||||||
|
# 평면 곡선반경 — 곡선부 확폭·최소곡선반지름 판정이 쓴다(2026-09-06).
|
||||||
|
"plan_radius_m": cross_section.get("plan_radius_m"),
|
||||||
"sample_count": len(samples),
|
"sample_count": len(samples),
|
||||||
"min_elevation_m": min(valid_z) if valid_z else None,
|
"min_elevation_m": min(valid_z) if valid_z else None,
|
||||||
"max_elevation_m": max(valid_z) if valid_z else None,
|
"max_elevation_m": max(valid_z) if valid_z else None,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from typing import Any
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from B05_Profile.B05_Profile_Engine_Geometry import circumradius_2d
|
||||||
from common_util.common_util_surface_sampler import SurfaceElevationSampler
|
from common_util.common_util_surface_sampler import SurfaceElevationSampler
|
||||||
from config.config_system import (
|
from config.config_system import (
|
||||||
SECTION_CROSS_HALF_WIDTH_M,
|
SECTION_CROSS_HALF_WIDTH_M,
|
||||||
@@ -116,6 +117,43 @@ def _float_or_none(value: float) -> float | None:
|
|||||||
return round(float(value), 6) if math.isfinite(float(value)) else None
|
return round(float(value), 6) if math.isfinite(float(value)) else None
|
||||||
|
|
||||||
|
|
||||||
|
#: 평면 곡선반경을 재는 앞뒤 거리(m). 측점 간격(20m)의 절반이라 한 측점의 곡률을
|
||||||
|
#: 이웃 측점에 번지지 않게 재고, 짧은 곡선도 놓치지 않는다.
|
||||||
|
PLAN_RADIUS_ARM_M = 10.0
|
||||||
|
#: 이보다 크면 직선으로 본다(m). 별표2 확폭표가 45m 이상을 "확폭 없음"으로 두므로
|
||||||
|
#: 그보다 넉넉한 값이면 판정에 영향이 없다.
|
||||||
|
PLAN_RADIUS_STRAIGHT_M = 10000.0
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_radii(
|
||||||
|
points: np.ndarray,
|
||||||
|
route_chainage: np.ndarray,
|
||||||
|
station_chainage: np.ndarray,
|
||||||
|
total: float,
|
||||||
|
) -> list[float | None]:
|
||||||
|
"""측점마다 평면 곡선반경(m). 직선·측정 불가는 None."""
|
||||||
|
arm = min(PLAN_RADIUS_ARM_M, max(total / 2.0, 0.0))
|
||||||
|
if arm < 0.5:
|
||||||
|
return [None] * len(station_chainage)
|
||||||
|
radii: list[float | None] = []
|
||||||
|
for value in station_chainage:
|
||||||
|
center = float(value)
|
||||||
|
back = max(0.0, center - arm)
|
||||||
|
ahead = min(total, center + arm)
|
||||||
|
# 시·종점에서는 한쪽 팔이 짧아진다 — 양쪽이 다 확보될 때만 잰다.
|
||||||
|
if center - back < arm * 0.5 or ahead - center < arm * 0.5:
|
||||||
|
radii.append(None)
|
||||||
|
continue
|
||||||
|
trio = _interpolate_xy(points, route_chainage, np.array([back, center, ahead]))
|
||||||
|
radius = circumradius_2d(trio[0], trio[1], trio[2])
|
||||||
|
radii.append(
|
||||||
|
None
|
||||||
|
if not math.isfinite(radius) or radius >= PLAN_RADIUS_STRAIGHT_M
|
||||||
|
else round(radius, 3)
|
||||||
|
)
|
||||||
|
return radii
|
||||||
|
|
||||||
|
|
||||||
def generate_sections(
|
def generate_sections(
|
||||||
polyline: np.ndarray | list[list[float]],
|
polyline: np.ndarray | list[list[float]],
|
||||||
sampler: SurfaceElevationSampler,
|
sampler: SurfaceElevationSampler,
|
||||||
@@ -167,6 +205,10 @@ def generate_sections(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
left_axes = np.column_stack([-tangents[:, 1], tangents[:, 0]])
|
left_axes = np.column_stack([-tangents[:, 1], tangents[:, 0]])
|
||||||
|
# 측점별 **평면 곡선반경**(m). 곡선부 확폭(별표2 Ⅰ.2.나.(4))과 최소곡선반지름 위반
|
||||||
|
# 표시가 이 값을 쓴다(2026-09-06). 노선 폴리라인 위에서 앞뒤로 같은 거리를 떨어진 세
|
||||||
|
# 점의 외접원 반경이며, 직선이면 무한대라 None 으로 낸다.
|
||||||
|
plan_radii = _plan_radii(points, route_chainage, station_chainage, total)
|
||||||
|
|
||||||
offsets = np.arange(
|
offsets = np.arange(
|
||||||
-options.cross_half_width_m,
|
-options.cross_half_width_m,
|
||||||
@@ -240,6 +282,7 @@ def generate_sections(
|
|||||||
"center_y": round(float(station_xy[index, 1]), 6),
|
"center_y": round(float(station_xy[index, 1]), 6),
|
||||||
"center_z": _float_or_none(center_z),
|
"center_z": _float_or_none(center_z),
|
||||||
"azimuth_deg": round(azimuth, 6),
|
"azimuth_deg": round(azimuth, 6),
|
||||||
|
"plan_radius_m": plan_radii[index],
|
||||||
# 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). 사용자 변경 시 확정에서 덮어쓴다.
|
# 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). 사용자 변경 시 확정에서 덮어쓴다.
|
||||||
"uphill_side": uphill_side,
|
"uphill_side": uphill_side,
|
||||||
"frame": frame,
|
"frame": frame,
|
||||||
|
|||||||
@@ -387,7 +387,11 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
|
|||||||
minUphillGrade.wrapper,
|
minUphillGrade.wrapper,
|
||||||
minDownhillGrade.wrapper,
|
minDownhillGrade.wrapper,
|
||||||
);
|
);
|
||||||
|
// [최적 경로 계산] 입구는 2026-09-06 사용자 확정으로 화면에서 내렸다 — 평면만 보고
|
||||||
|
// 노선을 정하는 방식이라 실제 판단(평면·종단·횡단·유토곡선을 함께 봄)과 맞지 않는다.
|
||||||
|
// 코드·API 는 그대로 두고 버튼만 뺀다. 노선 변경은 계획노선 편집이 대신한다.
|
||||||
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled");
|
||||||
|
solveButton.hidden = true;
|
||||||
routeCalc.body.append(
|
routeCalc.body.append(
|
||||||
algorithmField.root,
|
algorithmField.root,
|
||||||
paved.wrapper,
|
paved.wrapper,
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ export interface AlignmentPolicy {
|
|||||||
max_grade_pct: number;
|
max_grade_pct: number;
|
||||||
curve_skip_delta_pct: number;
|
curve_skip_delta_pct: number;
|
||||||
paved: boolean;
|
paved: boolean;
|
||||||
|
/** 법정 평면 최소곡선반지름(m) — 위반 표시 기준. 0이면 판정하지 않는다(옛 저장분). */
|
||||||
|
min_plan_radius_m?: number;
|
||||||
|
/** 배향곡선 하한(m) — 이보다 급하면 경고만 낸다(2026-09-06 사용자 확정). */
|
||||||
|
hairpin_min_radius_m?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlignmentNode {
|
export interface AlignmentNode {
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export interface BalanceBarParams {
|
|||||||
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
|
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
|
||||||
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
|
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
|
||||||
massHaul?: MassHaulSummaryValues | null;
|
massHaul?: MassHaulSummaryValues | null;
|
||||||
|
/** 법정 평면 최소곡선반지름 위반(2026-09-06) — 자동 보정 없이 **경고만** 낸다. */
|
||||||
|
planCurve?: { count: number; worstRadiusM: number; limitM: number; hairpin: number } | null;
|
||||||
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
||||||
onResetAll: () => void;
|
onResetAll: () => void;
|
||||||
}
|
}
|
||||||
@@ -93,6 +95,16 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
if (mass) {
|
if (mass) {
|
||||||
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
|
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
|
||||||
}
|
}
|
||||||
|
// 평면 곡선반경 — 법정 하한을 밑도는 측점이 있을 때만 적는다(별표2 Ⅰ.2.다.(1)).
|
||||||
|
// 프로그램은 값만 드러내고 고치지 않는다: 노선을 바꿀지는 사용자 판단이다.
|
||||||
|
const plan = params.planCurve;
|
||||||
|
if (plan && plan.count > 0) {
|
||||||
|
entries.push([
|
||||||
|
"곡선반경 부족",
|
||||||
|
`${plan.count} 곳 / 최소 ${plan.worstRadiusM.toFixed(1)} m`,
|
||||||
|
"over",
|
||||||
|
]);
|
||||||
|
}
|
||||||
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
|
||||||
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]);
|
||||||
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
|
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
|
||||||
@@ -107,6 +119,15 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
|||||||
item.append(caption, document.createTextNode(value));
|
item.append(caption, document.createTextNode(value));
|
||||||
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
|
||||||
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
|
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
|
||||||
|
if (label === "곡선반경 부족" && plan) {
|
||||||
|
item.title = [
|
||||||
|
`법정 하한 ${plan.limitM.toFixed(1)}m 미만인 측점 ${plan.count}곳`,
|
||||||
|
`가장 급한 곳 R=${plan.worstRadiusM.toFixed(1)}m`,
|
||||||
|
plan.hairpin > 0
|
||||||
|
? `그중 배향곡선 하한 미만 ${plan.hairpin}곳 — 경고만 낸다(자동 보정 없음)`
|
||||||
|
: "배향곡선 하한 미만은 없음",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
if (label === "누가토량" && mass) {
|
if (label === "누가토량" && mass) {
|
||||||
item.title = [
|
item.title = [
|
||||||
`절토(자연) ${volume(mass.cutNaturalM3)} · 절토(다짐) ${volume(mass.cutCompactedM3)}`,
|
`절토(자연) ${volume(mass.cutNaturalM3)} · 절토(다짐) ${volume(mass.cutCompactedM3)}`,
|
||||||
|
|||||||
@@ -325,6 +325,34 @@ export function createRouteProfilePanel(
|
|||||||
return samples[samples.length - 1][field];
|
return samples[samples.length - 1][field];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 법정 평면 최소곡선반지름을 밑도는 측점을 센다(별표2 Ⅰ.2.다.(1), 2026-09-06).
|
||||||
|
* 반경은 횡단 측점마다 실려 오고(`plan_radius_m`), 하한은 계획선 정책이 들고 있다.
|
||||||
|
* **경고만** 낸다 — 노선을 고칠지는 사용자 판단이다.
|
||||||
|
*/
|
||||||
|
function planCurveWarning(): {
|
||||||
|
count: number;
|
||||||
|
worstRadiusM: number;
|
||||||
|
limitM: number;
|
||||||
|
hairpin: number;
|
||||||
|
} | null {
|
||||||
|
const limit = alignment?.policy.min_plan_radius_m ?? 0;
|
||||||
|
const sections = detail?.cross_sections ?? [];
|
||||||
|
if (!limit || !sections.length) return null;
|
||||||
|
const hairpinLimit = alignment?.policy.hairpin_min_radius_m ?? 0;
|
||||||
|
let count = 0;
|
||||||
|
let hairpin = 0;
|
||||||
|
let worst = Number.POSITIVE_INFINITY;
|
||||||
|
sections.forEach((section) => {
|
||||||
|
const radius = section.plan_radius_m;
|
||||||
|
if (typeof radius !== "number" || !Number.isFinite(radius) || radius >= limit) return;
|
||||||
|
count += 1;
|
||||||
|
worst = Math.min(worst, radius);
|
||||||
|
if (hairpinLimit > 0 && radius < hairpinLimit) hairpin += 1;
|
||||||
|
});
|
||||||
|
return count ? { count, worstRadiusM: worst, limitM: limit, hairpin } : null;
|
||||||
|
}
|
||||||
|
|
||||||
function renderBalance(): void {
|
function renderBalance(): void {
|
||||||
const minCoverViolations = findMinCoverViolations(
|
const minCoverViolations = findMinCoverViolations(
|
||||||
minCoverTargets,
|
minCoverTargets,
|
||||||
@@ -332,6 +360,7 @@ export function createRouteProfilePanel(
|
|||||||
(chainageM) => sampleAt(chainageM, "elevation_m"),
|
(chainageM) => sampleAt(chainageM, "elevation_m"),
|
||||||
);
|
);
|
||||||
renderBalanceBar({
|
renderBalanceBar({
|
||||||
|
planCurve: planCurveWarning(),
|
||||||
minCoverViolations,
|
minCoverViolations,
|
||||||
balanceBar,
|
balanceBar,
|
||||||
tools: tools.render(),
|
tools: tools.render(),
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ export interface SectionStation {
|
|||||||
structure?: string;
|
structure?: string;
|
||||||
center_z: number | null;
|
center_z: number | null;
|
||||||
azimuth_deg: number | null;
|
azimuth_deg: number | null;
|
||||||
|
/** 평면 곡선반경(m). 직선이거나 잴 수 없으면 null — 곡선부 확폭·법정 최소반경 판정용. */
|
||||||
|
plan_radius_m?: number | null;
|
||||||
center_x: number;
|
center_x: number;
|
||||||
center_y: number;
|
center_y: number;
|
||||||
/** 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). B05 solve 자동 판정 + 사용자 변경. */
|
/** 횡단 기준 등고가 높은 쪽(측구 설계 기본 방향). B05 solve 자동 판정 + 사용자 변경. */
|
||||||
|
|||||||
@@ -442,6 +442,17 @@ FOREST_ROAD_PROFILE_CRITERIA = {
|
|||||||
"min_curve_length_m": 20.0,
|
"min_curve_length_m": 20.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
# 평면 최소곡선반지름(m, 별표2 Ⅰ.2.다.(1)) — 설계속도·지형별. **경로탐색 제약이 아니라
|
||||||
|
# 위반 표시 기준**이다(2026-09-06 사용자 확정: 자동탐색은 쓰지 않고 계획노선을 직접
|
||||||
|
# 고친다). 탐색이 쓰는 등급별 상수(FOREST_ROAD_MIN_CURVE_R_M)와 혼용하지 않는다.
|
||||||
|
"min_plan_radius_m": {
|
||||||
|
40: {"normal": 60.0, "special": 40.0},
|
||||||
|
30: {"normal": 30.0, "special": 20.0},
|
||||||
|
20: {"normal": 15.0, "special": 12.0},
|
||||||
|
},
|
||||||
|
# 배향곡선(Hair Pin) 중심선 반지름 하한(m, 별표2 Ⅰ.2.다.(2)). 이보다 급하면 **경고만**
|
||||||
|
# 낸다 — 자동 보정·차단은 하지 않는다(2026-09-06 사용자 확정).
|
||||||
|
"hairpin_min_radius_m": 10.0,
|
||||||
# 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이
|
# 임도 종류 → **기본** 설계속도(km/h). 임도는 속도를 낼 수 없는 노선이라 20이
|
||||||
# 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서
|
# 기본이다(2026-08-19 사용자 확정). 별표2상 간선·산불진화는 20~40 범위에서
|
||||||
# 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른
|
# 설계자가 고르고, 작업임도는 20 이하이므로 20 고정이다. 사용자가 화면에서 고른
|
||||||
|
|||||||
Reference in New Issue
Block a user