Files
Aislo/common_util/common_util_mass_haul_sites.ts
T
eomsangdonandClaude Opus 5 6c90c26eef feat(B05): 계획 유토곡선 이관 + 사토장·토취장 4옵션 자동 선정
절·성토 균형은 종단 시공계획고로 정해지고 사토장 위치도 계획고를 다시 끌어야
정리되므로, 계획용 유토곡선과 부지 선정을 B05로 옮겼다. B06은 실측 단면적으로
낸 정식 곡선과 확정·B08 인계를 맡는다.

B05 계획 유토곡선
- _UI_Profile_MassHaul.ts: 종단면 패널 안 2차 하단 슬라이드. 펼치면 12행 도면
  테이블 자리를 곡선이 대신 차지한다. 곡선 SVG를 종단 그래프와 같은 가로
  스크롤러 안 형제로 넣고 MassHaulAxis에 LONG_PAD + originOffset을 넘겨 X축을
  종단 chainageMapper와 일치시켰다.
- computeLongitudinalMassHaul(): 횡단 설계가 없는 계획 단계용 개략 엔진. 표준횡단
  노반폭을 전 구간 공통으로 물린다. 종단 기반 토량은 국내 기준상 노선계획 개산용
  이므로(2026-08-03 조사) 표준단면까지 씌워 정밀화하지 않는다.

사토장·토취장 자동 선정
- B05_wf2_Route_Engine_Disposal.py + POST /route/disposal-sites: 노선 corridor
  DEM 격자를 훑어 후보 부지를 추리고 옵션별 점수로 정렬한다. 기준 4가지 —
  비용(최단거리+하향 운반), 지형안정성(완경사·계곡 이격), 계곡부(계곡 축 매립),
  임내 공간(라이다 수고 기반 공터). 기준값 정의처는 DISPOSAL_SITE_CRITERIA.
- _UI_Profile_Disposal.ts: 범례 줄의 [토량 분배]와 [도형 위치 초기화] 사이에
  구분기호와 함께 라디오 토글 4개. 부지 카드에 위치·용량·운반거리와 기준별 근거값,
  측점·용량 사용자 정의 편집과 복원. 후보 부족으로 남은 토량은 경고로 알린다.
- 확정 시 종단 정본의 disposal_sites에 심는다(기준 미선택이면 저장분 삭제).

B06 연동
- common_util_mass_haul_sites.ts: 저장된 부지를 정식 곡선의 잔량 위치 기준으로
  운반거리를 다시 재 표시하고 mass_haul.disposal_sites로 B08에 넘긴다.

미구현(계획서에 남김): 토취장 토질 판정(지반유형이 B06 산출물), 계곡부 암거 연장.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 19:06:46 +09:00

127 lines
5.0 KiB
TypeScript

/* =============================================================================
* common_util_mass_haul_sites.ts
* 사토장·토취장 선정 결과를 유토곡선 옆에 표시하고 운반거리를 다시 재는 공용 모듈.
*
* 부지를 **고르는** 일은 B05 계획 단계에서 끝난다(계획선을 바꿔야 정리되는 문제라서다).
* B06은 그 결과를 그대로 받아, 실측 단면적으로 낸 **정식 곡선**의 잔량 위치를 기준으로
* 운반거리를 다시 재 표시하고 B08 내역서로 넘긴다 — 계획 곡선과 정식 곡선은 잔량이
* 놓이는 자리가 다르므로, B05가 적어 둔 거리를 그대로 쓰면 물량과 어긋난다.
* ========================================================================== */
import type { HaulPlan } from "./common_util_mass_haul_balance";
/** B05가 종단 정본(`longitudinal.disposal_sites`)에 심어 둔 부지 하나. */
export interface DisposalSiteRecord {
id: string;
kind: "spoil" | "borrow";
chainage_m: number;
offset_m: number;
side: "left" | "right";
x: number;
y: number;
elevation_m: number;
capacity_m3: number;
assigned_m3: number;
/** B05 계획 곡선 기준 운반거리(m). 표시·저장에는 아래 재계산값을 쓴다. */
haul_distance_m: number;
/** 사용자가 위치나 용량을 손으로 고친 부지인가. */
custom: boolean;
}
export interface DisposalSitesSnapshot {
/** 선정 기준 — `cost` | `stability` | `valley` | `clearing`. */
option: string;
sites: DisposalSiteRecord[];
}
const OPTION_LABEL: Record<string, string> = {
cost: "비용",
stability: "지형안정성",
valley: "계곡부",
clearing: "임내 공간",
};
/** 잔량이 몰려 있는 대표 위치(m). 없으면 null. */
function residualCenter(plan: HaulPlan | null, kind: "spoil" | "borrow"): number | null {
const residuals = (plan?.residuals ?? []).filter((residual) => residual.kind === kind);
const total = residuals.reduce(
(sum, residual) =>
sum +
(kind === "spoil"
? Math.max(residual.volume_m3 - residual.natural_m3, 0)
: residual.volume_m3),
0,
);
if (total <= 0) return null;
const weighted = residuals.reduce((sum, residual) => {
const volume =
kind === "spoil" ? Math.max(residual.volume_m3 - residual.natural_m3, 0) : residual.volume_m3;
return sum + volume * 0.5 * (residual.from_m + residual.to_m);
}, 0);
return weighted / total;
}
/** 정식 곡선의 잔량 위치를 기준으로 각 부지의 운반거리를 다시 잰 목록. */
export function resolveDisposalSites(
snapshot: DisposalSitesSnapshot | null | undefined,
plan: HaulPlan | null,
): DisposalSiteRecord[] {
if (!snapshot?.sites?.length) return [];
const centers = {
spoil: residualCenter(plan, "spoil"),
borrow: residualCenter(plan, "borrow"),
};
return snapshot.sites.map((site) => {
const center = centers[site.kind];
if (center === null) return site;
const distance = Math.abs(site.chainage_m - center) + Math.abs(site.offset_m);
return { ...site, haul_distance_m: Math.round(distance * 10) / 10 };
});
}
/**
* 곡선 아래 요약 줄 다음에 붙는 사토장·토취장 줄. 선정 결과가 없으면 null.
* 표시 수치는 정식 곡선 기준으로 다시 잰 값이라 B05 화면과 거리가 다를 수 있다.
*/
export function createDisposalSummary(
snapshot: DisposalSitesSnapshot | null | undefined,
plan: HaulPlan | null,
): HTMLElement | null {
const sites = resolveDisposalSites(snapshot, plan);
if (!sites.length) return null;
const row = document.createElement("div");
row.className = "b06-masshaul__summary b06-masshaul__summary--sites";
const basis = document.createElement("span");
basis.className = "b06-masshaul__basis";
basis.textContent = `선정 기준 ${OPTION_LABEL[snapshot?.option ?? ""] ?? snapshot?.option ?? "-"}`;
row.append(basis);
for (const site of sites) {
const chip = document.createElement("span");
chip.className = "b06-masshaul__chip";
const name = document.createElement("em");
name.textContent = `${site.kind === "spoil" ? "사토장" : "토취장"}${site.custom ? "*" : ""}`;
const value = document.createElement("strong");
value.textContent =
`${site.chainage_m.toFixed(1)}m ${site.side === "left" ? "좌" : "우"}` +
`${Math.abs(site.offset_m).toFixed(0)}m · ${site.assigned_m3.toLocaleString()}㎥ · ` +
`L=${site.haul_distance_m.toFixed(0)}m`;
chip.append(name, value);
row.append(chip);
}
const note = document.createElement("span");
note.className = "b06-masshaul__chip";
note.textContent = "* 사용자 정의";
row.append(note);
return row;
}
/** 확정 저장·B08 인계용 직렬화. 운반거리는 정식 곡선 기준으로 다시 잰 값을 싣는다. */
export function disposalSitesPayload(
snapshot: DisposalSitesSnapshot | null | undefined,
plan: HaulPlan | null,
): Record<string, unknown> | undefined {
const sites = resolveDisposalSites(snapshot, plan);
if (!sites.length) return undefined;
return { option: snapshot?.option, sites };
}