- 구간 끝 · 관 측점이 격자 자리를 떠나면 그 자리 새 일반 측점과 번호 겹침(station_000000080000 둘) — 옛 자리에 다른 측점이 서면 새 번호 · 아니면 옛 번호(433-4 MC) - 갈아끼움이 preview 표시를 들고 가면 다음 측점 맞춤이 그 측점을 버림(시점 측점 사라짐 실측) — 지움 - 시험: test_458 카드 번호 · preview 둘 더 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
279 lines
13 KiB
TypeScript
279 lines
13 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Station_Swap.ts
|
|
* 측점 수가 바뀔 때 횡단 목록 갈아끼움(PLAN 346-2 4파) — 계산 묶음 측점(`PipelineStation[]`) → 새 상세 객체.
|
|
*
|
|
* · 같은 측점(열쇠 · MC 가 움직여 바뀐 곡선 열쇠 `curveMoves` · 없으면 같은 자리 같은 종류)은 앞 횡단을 이어
|
|
* 받음 — 설계 · 구조물 제원 · 이름 그대로 · 자리(누가거리 · 좌표 · 방위 · 확폭)만 새것(433-4).
|
|
* · 지반 표본 · 상단측 · 중심고 = 자리 · 축이 같은 앞 횡단이면 앞 값(서버 샘플링 정본 · 덧 측점만 넣어도 나머지
|
|
* 측점 면적이 브라우저 지형으로 안 바뀜 · 433-11) · 새 측점 · 옮긴 측점만 지반 표본(채워 왔으면).
|
|
* · 새 측점 = 지반 표본 + 기본 설계(계획고 0 → 낡음 → 브라우저 다시 셈이 곧 채움).
|
|
* · 상단측 = 새 지반 표본으로 다시 셈한 측점은 다시(단면 유형 기본값 재료 · 서버 횡단 파일 `uphill_side` 짝 · 428).
|
|
* · 0.5 m 넘게 옮긴 관 · 구조물 측점 = 기본 설계 + 사용자 칸(만진 칸 · 지반 등 입력 `ROW_CARRY_KEYS`) — 서버가
|
|
* 그 행을 지우고 기본 설계에 [저장] patch 를 얹음(`sync_cross_section_rows` · `compute_default_designs` · 428) ·
|
|
* patch 가 그 칸을 모두 실음(`crossPatchesFromCache` · 445 H2).
|
|
* · 격자 아닌 자리 = 앞 횡단의 `grid` 표시 · 초안 사본 표시(`preview`)를 지움 · 카드 이름은 형상 다시 얹기가
|
|
* 계획 제목으로(458).
|
|
* · 빠진 측점 = 목록에서 뺌(세션 고침은 열쇠로 남음 · 노선이 바뀐 때만 「노선 밖 고침」 · 부르는 쪽).
|
|
* · 옛 상세는 그대로 둠 — 새 객체를 돌려줌(쥔 쪽이 갈아끼움).
|
|
*
|
|
* ⚠ 파이썬·TS 짝 — `B05_Profile_Engine_Sections_Core.py` `format_station` ↔ `formatStation`.
|
|
* 거울 시험 `resources/tester/test_346_2_station_swap.py`.
|
|
* 순수 계산(DOM · fetch · 세션 없음).
|
|
* ========================================================================== */
|
|
|
|
import type { PipelineStation } from "../../A00_Common/design/logic/design_pipeline_types";
|
|
import type { CrossSection, SectionDetailResponse, SectionStation } from "../B06_Section_Api_Fetch";
|
|
import { uphillSide } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
|
import { curveMoves } from "../../common_util/common_util_station_key";
|
|
|
|
interface Sample {
|
|
offset_m: number;
|
|
elevation_m: number | null;
|
|
valid: boolean;
|
|
}
|
|
|
|
/** 지반 표본(중심 → 바깥 · 좌 +) → 엔진 표본(오프셋 오름차순 · 우 −). */
|
|
export function groundSamples(station: PipelineStation): Sample[] {
|
|
const ground = station.ground;
|
|
if (!ground) return [];
|
|
const point = (offset: number, z: number | null): Sample => ({
|
|
offset_m: offset,
|
|
elevation_m: z,
|
|
valid: z !== null && Number.isFinite(z),
|
|
});
|
|
const right = ground.right.map((s) => point(-s.offsetM, s.z));
|
|
const left = ground.left.map((s) => point(s.offsetM, s.z));
|
|
const out = [...right, ...left];
|
|
if (ground.centerZ !== null && !out.some((s) => Math.abs(s.offset_m) < 1e-9))
|
|
out.push(point(0, ground.centerZ));
|
|
return out.sort((a, b) => a.offset_m - b.offset_m);
|
|
}
|
|
|
|
/** 짝: `format_station` — `STA.k+mmm.mmm`. */
|
|
export function formatStation(chainageM: number): string {
|
|
const at = Math.max(chainageM, 0);
|
|
const km = Math.floor(at / 1000);
|
|
const rest = (at - km * 1000).toFixed(3).padStart(7, "0");
|
|
return `STA.${km}+${rest}`;
|
|
}
|
|
|
|
/** 서버 측점 id 꼴(`station_000000484210` = 누가거리 mm 12자리). */
|
|
export const stationIdOf = (chainageM: number): string =>
|
|
`station_${String(Math.round(chainageM * 1000)).padStart(12, "0")}`;
|
|
|
|
/** 새 측점의 기본 설계 — 계획고 0 이라 늘 낡음 → 다시 셈이 세션 선택 · 기본값으로 채움 · 단면 유형 = 상단측 깎기. */
|
|
const freshDesign = (uphill: CrossSection["uphill_side"]) =>
|
|
({
|
|
design_elevation_m: 0,
|
|
ground_type: "ripping_rock",
|
|
section_mode: `${uphill ?? "left"}_cut`,
|
|
ditch_type: "standard",
|
|
}) as unknown as NonNullable<CrossSection["design"]>;
|
|
|
|
/** 서버가 행을 지우고 새로 세우는 옮김 폭(m) — `sync_cross_section_rows` 의 0.5 m 짝. */
|
|
const FACILITY_RESEED_M = 0.5;
|
|
/** 다시 셈 입력 중 새 지형이 정하는 칸 — 옮긴 시설 측점은 버림(단면 유형은 기본 설계 · 서버 `CARRY_INPUT_KEYS` 밖). */
|
|
const AUTO_INPUT_KEYS = ["ditch_side", "ditch_enabled"];
|
|
/**
|
|
* 옮긴 측점이 들고 가는 사용자 입력(445 H2) — 서버 `CARRY_INPUT_KEYS` 짝(노선 [확인] 이월과 같은 칸).
|
|
* 서버에 그 자리 행이 없으면(옮김 · 새 측점) [저장] patch 가 캐시 설계의 이 칸을 실음.
|
|
*/
|
|
export const ROW_CARRY_KEYS = [
|
|
"ground_type",
|
|
"ditch_type",
|
|
"ditch_choice",
|
|
"two_stage_slope",
|
|
"rock_boundary_offset_m",
|
|
"cut_slope_ratio_user",
|
|
"berm",
|
|
] as const;
|
|
|
|
/** 옮긴 시설 측점 설계 — 기본 설계 위에 앞 설계의 나머지(사용자 칸 · 지반 등 입력) · 결과 칸은 다시 셈이 갈음. */
|
|
function reseeded(
|
|
prior: CrossSection["design"],
|
|
uphill: CrossSection["uphill_side"],
|
|
): NonNullable<CrossSection["design"]> {
|
|
const before = (prior ?? {}) as Record<string, unknown>;
|
|
const design = { ...before, ...freshDesign(uphill) } as Record<string, unknown>;
|
|
for (const key of ROW_CARRY_KEYS) if (before[key] !== undefined) design[key] = before[key];
|
|
for (const key of AUTO_INPUT_KEYS) delete design[key];
|
|
return design as unknown as NonNullable<CrossSection["design"]>;
|
|
}
|
|
|
|
function kindOf(station: PipelineStation): SectionStation["kind"] {
|
|
if (station.kind === "end") return station.key.endsWith("EP") ? "ep" : "bp";
|
|
if (station.kind === "curve") return "curve";
|
|
return station.kind === "grid" ? "regular" : "irregular";
|
|
}
|
|
|
|
/** 같은 목록인가 — 열쇠 · 누가거리가 차례대로 같음(제자리 갱신이면 갈아끼우지 않음). */
|
|
export function sameStationList(
|
|
sections: ReadonlyArray<{ chainage_m: number }>,
|
|
oldKeys: ReadonlyArray<string>,
|
|
stations: ReadonlyArray<PipelineStation>,
|
|
): boolean {
|
|
return (
|
|
sections.length === stations.length &&
|
|
stations.every(
|
|
(station, index) =>
|
|
Math.abs(station.chainageM - sections[index].chainage_m) < 1e-6 &&
|
|
(oldKeys[index] === station.key || kindOf(station) === "regular"),
|
|
)
|
|
);
|
|
}
|
|
|
|
/** 조작값 맵 열쇠(`85.05` · `85.05:outlet` · `b85.05`)의 누가거리를 새 자리로 — 안 옮기는 열쇠는 그대로. */
|
|
export function renameChainageKeys<V>(
|
|
values: ReadonlyMap<string, V>,
|
|
moves: ReadonlyMap<string, string>,
|
|
): Map<string, V> {
|
|
const out = new Map<string, V>();
|
|
for (const [key, value] of values) {
|
|
const match = /^(b?)(-?\d+\.\d{2})(:.*)?$/.exec(key);
|
|
const to = match && moves.get(match[2]);
|
|
out.set(to ? `${match[1]}${to}${match[3] ?? ""}` : key, value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export interface StationSwap {
|
|
detail: SectionDetailResponse;
|
|
added: string[];
|
|
removed: string[];
|
|
/** 이어 받은 측점 중 누가거리가 바뀐 것 — 옛 0.01 m 글 → 새 글(조작값 맵 옮김). */
|
|
moves: Map<string, string>;
|
|
/** 이어 받은 관 · 구조물 밖 측점 중 좌표가 0.01 m 넘게 움직인 것이 있나 — 노선이 바뀐 차례(「노선 밖 고침」 셈). */
|
|
moved: boolean;
|
|
}
|
|
|
|
/** 앞 횡단과 자리(누가거리 · 좌표 0.01 m) · 축(왼쪽 단위 벡터)이 같은가 — 같으면 앞 지반 표본을 그대로 씀. */
|
|
function samePlace(prior: CrossSection, station: PipelineStation): boolean {
|
|
const [lx, ly] = prior.frame?.left_xy ?? [Number.NaN, Number.NaN];
|
|
return (
|
|
Math.abs(prior.chainage_m - station.chainageM) < 0.01 &&
|
|
Math.hypot(prior.center_x - station.x, prior.center_y - station.y) <= 0.01 &&
|
|
Math.abs(lx - station.left[0]) < 1e-4 &&
|
|
Math.abs(ly - station.left[1]) < 1e-4
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 새 측점 목록으로 상세를 새로 짬. `oldKeys` = 지금 상세 측점마다 열쇠(같은 차례 · `stationKeysOf`) ·
|
|
* 열쇠가 안 맞으면 옮긴 곡선 열쇠(`curveMoves` · 서버 `carry_cross_inputs` 같은 규칙) · 그다음 같은 자리(0.01 m) ·
|
|
* 같은 종류의 앞 횡단을 씀.
|
|
*/
|
|
export function swapCrossSections(
|
|
detail: SectionDetailResponse,
|
|
stations: ReadonlyArray<PipelineStation>,
|
|
oldKeys: ReadonlyArray<string>,
|
|
intervalM: number | null,
|
|
): StationSwap {
|
|
const byKey = new Map(detail.cross_sections.map((section, i) => [oldKeys[i], section]));
|
|
const used = new Set<CrossSection>();
|
|
let moved = false;
|
|
const added: string[] = [];
|
|
const moves = new Map<string, string>();
|
|
const text = (chainageM: number) => chainageM.toFixed(2);
|
|
// 새 곡선 열쇠 → 옛 곡선 열쇠(R 바꿔 MC 가 움직임 · 433-4)
|
|
const curveFrom = new Map(
|
|
[
|
|
...curveMoves(
|
|
detail.cross_sections.map((section, i) => [oldKeys[i], section.chainage_m] as const),
|
|
stations.map((station) => [station.key, station.chainageM] as const),
|
|
),
|
|
].map(([from, to]) => [to, from]),
|
|
);
|
|
const priorOf = (station: PipelineStation): CrossSection | undefined => {
|
|
const from = curveFrom.get(station.key);
|
|
const hit = byKey.get(station.key) ?? (from === undefined ? undefined : byKey.get(from));
|
|
if (hit && !used.has(hit)) return hit;
|
|
return detail.cross_sections.find(
|
|
(section) =>
|
|
!used.has(section) &&
|
|
section.kind === kindOf(station) &&
|
|
Math.abs(section.chainage_m - station.chainageM) < 0.01,
|
|
);
|
|
};
|
|
const longStations: SectionStation[] = [];
|
|
const cross_sections = stations.map((station): CrossSection => {
|
|
const prior = priorOf(station);
|
|
if (prior) used.add(prior);
|
|
else added.push(station.key);
|
|
// 노선이 바뀜 = 관 · 구조물 밖 측점이 움직임 — 시설만 옮기면 고침을 열쇠째 옮김(415 H1)
|
|
if (
|
|
prior &&
|
|
station.kind !== "pipe" &&
|
|
station.kind !== "structure" &&
|
|
Math.hypot(prior.center_x - station.x, prior.center_y - station.y) > 0.01
|
|
)
|
|
moved = true;
|
|
if (prior && text(prior.chainage_m) !== text(station.chainageM))
|
|
moves.set(text(prior.chainage_m), text(station.chainageM));
|
|
// 자리 같은 앞 횡단 = 서버 표본 그대로(433-11) · 아니면 새 지반 표본(채워 왔으면)
|
|
const fresh = !!station.ground && !(prior?.samples?.length && samePlace(prior, station));
|
|
const kind = kindOf(station);
|
|
const onGrid =
|
|
kind === "irregular" &&
|
|
!!intervalM &&
|
|
Math.abs(station.chainageM / intervalM - Math.round(station.chainageM / intervalM)) < 1e-6;
|
|
// 옮긴 측점의 옛 자리에 다른 측점이 섬(격자 자리를 떠난 구간 끝 등) = 새 번호 — 옛 번호를 들고 가면 그 자리 새
|
|
// 측점 번호(`stationIdOf`)와 겹침(458) · 안 겹치면 옛 번호(MC 이동 · 433-4)
|
|
const vacated =
|
|
!!prior &&
|
|
Math.abs(prior.chainage_m - station.chainageM) >= 0.01 &&
|
|
stations.some(
|
|
(other) => other !== station && Math.abs(other.chainageM - prior.chainage_m) < 0.0005,
|
|
);
|
|
const place = {
|
|
station_id: prior && !vacated ? prior.station_id : stationIdOf(station.chainageM),
|
|
chainage_m: station.chainageM,
|
|
label: formatStation(station.chainageM),
|
|
kind,
|
|
...(onGrid ? { grid: true } : {}),
|
|
center_z: fresh
|
|
? (station.ground?.centerZ ?? prior?.center_z ?? null)
|
|
: (prior?.center_z ?? null),
|
|
azimuth_deg: station.azimuthDeg,
|
|
plan_radius_m: station.planRadiusM,
|
|
curve_outer_side: station.curveOuterSide,
|
|
curve_widening_m: station.curveWideningM,
|
|
center_x: station.x,
|
|
center_y: station.y,
|
|
frame: { left_xy: [station.left[0], station.left[1]] as [number, number] },
|
|
};
|
|
longStations.push({ ...place, ...(prior?.structure ? { structure: prior.structure } : {}) });
|
|
const samples = fresh ? groundSamples(station) : (prior?.samples ?? []);
|
|
const uphill = fresh ? uphillSide(station) : (prior?.uphill_side ?? null);
|
|
if (!prior) return { ...place, uphill_side: uphill, samples, design: freshDesign(uphill) };
|
|
const reseed =
|
|
(station.kind === "pipe" || station.kind === "structure") &&
|
|
Math.abs(prior.chainage_m - station.chainageM) > FACILITY_RESEED_M;
|
|
const next: CrossSection = {
|
|
...prior,
|
|
...place,
|
|
uphill_side: uphill,
|
|
samples,
|
|
...(reseed ? { design: reseeded(prior.design, uphill) } : {}),
|
|
};
|
|
if (!onGrid) delete next.grid; // 격자 자리를 떠난 시설 측점 — 옛 숨은 일반 측점 표시를 안 들고 감(458)
|
|
// 묶음 측점 = 정본 목록 — 초안 맞춤의 「구조물 없는 사본」 표시를 안 들고 감(들고 가면 다음 맞춤이 그 측점을 버림)
|
|
delete next.preview;
|
|
return next;
|
|
});
|
|
const removed = detail.cross_sections
|
|
.map((section, i) => (used.has(section) ? null : oldKeys[i]))
|
|
.filter((key): key is string => key !== null);
|
|
const last = stations[stations.length - 1]?.chainageM ?? detail.longitudinal.length_m;
|
|
return {
|
|
detail: {
|
|
...detail,
|
|
longitudinal: { ...detail.longitudinal, stations: longStations, length_m: last },
|
|
cross_sections,
|
|
},
|
|
added,
|
|
removed,
|
|
moved,
|
|
moves,
|
|
};
|
|
}
|