- 노선 저장 직전 저장 관(id · 옛 누가거리)을 떠 두고 저장 뒤 같은 id 새 자리로 — 초안으로 안 따라간 관 조절(첫 초안 측점 짝 · 관 id 규칙 갈림) - carryChainageStates 시설 풀이 = 옛 자리 → 새 자리 · 관 열쇠 → 새 자리 - 시험 test_417 옛 자리 짝 · 346-3 저장 차례(관 받기) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
457 lines
21 KiB
TypeScript
457 lines
21 KiB
TypeScript
/* =============================================================================
|
|
* B06_Section_Station_Keys.ts
|
|
* 세션 횡단 초안의 **측점 열쇠**(PLAN 344-3) — 노선이 바뀌어 누가거리가 밀려도 같은 측점의 고침이 남게.
|
|
*
|
|
* 열쇠 규칙 = `common_util_station_key`(일반 grid:k · 관 pipe:<id> · 구조물 structure:<id> · 시종점 end:BP/EP ·
|
|
* id 모름 at:<0.01 m>). 담는 칸 다섯(crossdesign · shoulderext · bermdepth · rockb · cutslope)은 측점 열쇠로
|
|
* 쓰고, 부르는 쪽에는 지금처럼 **누가거리**로 내줌(읽기 · 쓰기 창구가 그대로).
|
|
*
|
|
* · 지금 측점 목록 = 상세가 초안 측점을 얹을 때마다(`useStationKeys` · Section_Drafts) — 관 id 는 관 세션
|
|
* 장부 · 구조물 id 는 측점 계획(초안) · 구조물 목록(횡단 보기)에서.
|
|
* · 칸마다 열쇠 → 쓴 때 누가거리 `@at` 을 함께 담음 — 구조물 id 를 아직 모를 때(첫 열기)도 그 자리 측점이면 읽음.
|
|
* · 옛 초안(누가거리 열쇠 `"85.05"`) · `at:` 열쇠는 읽을 때 지금 측점 열쇠로 옮겨 다시 씀.
|
|
* · 노선이 바뀜 = `carryStationDrafts` — 넘기는 칸 = 서버 `CARRY_INPUT_KEYS` 일곱(지반 유형 · 측구 종류 ·
|
|
* 측구 선택 · 2단 경사 · 암 경계 · 암 경사 · 소단 구간은 구간 그대로) + 사면 쉬프트 · 소단 깊이(417 · 서버
|
|
* `ROUTE_CARRY_KEYS`) · 단면 유형 · 측구 방향은 새 지형이 정함 · 사라진 측점의 고침은 버리고 「노선 밖 고침 N 건」.
|
|
* · 노선 저장 = `carryChainageStates` — 누가거리 열쇠 칸(옹벽 · 세월 · BOX · 집수 조절 · 측점 폭 · 메모 · 상세 표 ·
|
|
* `ROUTE_CARRY_STATES`)도 측점 열쇠로 새 노선 측점에 옮김(구조물 조절 = 관 · 구조물 열쇠만 · 서버와 같음).
|
|
* ========================================================================== */
|
|
|
|
import { readState, writeState, type StateName } from "../A00_Common/b_page_state";
|
|
import { pipeIdAt, pipeIdNear } from "../A00_Common/design/design_pipe_ids";
|
|
import { showToast } from "@ui/ui_template_elements";
|
|
import {
|
|
chainageKey,
|
|
dedupeStationKeys,
|
|
looseChainage,
|
|
stationKey,
|
|
type StationKeyRole,
|
|
} from "@util/common_util_station_key";
|
|
import { inferStationInterval } from "@util/common_util_svg";
|
|
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
|
|
|
/** 측점 열쇠로 담는 세션 칸. */
|
|
export type KeyedState = "crossdesign" | "shoulderext" | "bermdepth" | "rockb" | "cutslope";
|
|
export const KEYED_STATES: readonly KeyedState[] = [
|
|
"crossdesign",
|
|
"shoulderext",
|
|
"bermdepth",
|
|
"rockb",
|
|
"cutslope",
|
|
];
|
|
|
|
/** 노선이 바뀔 때 넘기는 카드 선택 — 서버 `CARRY_INPUT_KEYS` 중 선택 칸(나머지 셋 = rockb · cutslope · 소단 구간). */
|
|
const CARRY_CHOICE_KEYS = ["ground_type", "ditch_type", "ditch_choice", "two_stage_slope"] as const;
|
|
/** 노선이 바뀔 때 통째로 넘기는 칸 — 사면 쉬프트(노견 확장) · 소단 깊이도(417 · 서버 `ROUTE_CARRY_KEYS`). */
|
|
const CARRY_WHOLE: readonly KeyedState[] = ["rockb", "cutslope", "shoulderext", "bermdepth"];
|
|
|
|
/**
|
|
* 노선 저장 때 새 노선 측점으로 옮기는 **누가거리 열쇠** 칸(측점 옮김 목록 · 417) → 담긴 설계 칸.
|
|
* ⚠ 짝 — 설계 칸 합 = 서버 `ROUTE_CARRY_KEYS`(측점 열쇠마다) + `STRUCTURE_ADJUST_KEYS`(관 · 구조물 열쇠만) ·
|
|
* 거울 시험 `resources/tester/test_417_route_carry_mirror.py`.
|
|
*/
|
|
export const ROUTE_CARRY_STATES = {
|
|
revetx: ["revet_adjust"],
|
|
basinadjust: ["basin_adjust"],
|
|
inletstruct: ["inlet_structure"],
|
|
extrawall: ["extra_wall_counts"],
|
|
extraspan: ["extra_spans"],
|
|
revetlink: ["revet_link_detached", "revet_follow_grade"],
|
|
fordadjust: ["ford_adjust"],
|
|
boxadjust: ["box_adjust"],
|
|
crossw: ["display_half_width_m"],
|
|
"structure-notes": ["structure_note"],
|
|
"structure-note-cads": ["structure_note_cad"],
|
|
"detail-table": ["detail_table"],
|
|
shoulderext: ["shoulder_extend_left_m", "shoulder_extend_right_m"],
|
|
bermdepth: ["berm_depth_left_m", "berm_depth_right_m"],
|
|
} as const;
|
|
/** 구조물 조절 칸 — 관 · 구조물 열쇠 측점만 따라감(서버 `carry_cross_inputs` 와 같음). */
|
|
const STRUCTURE_STATES = new Set<string>([
|
|
"revetx",
|
|
"basinadjust",
|
|
"inletstruct",
|
|
"extrawall",
|
|
"extraspan",
|
|
"revetlink",
|
|
"fordadjust",
|
|
"boxadjust",
|
|
]);
|
|
|
|
/** 같은 측점으로 보는 거리 — 옛 초안 열쇠(0.01 m) · 서버 측점(0.001 m) 차이를 받음. */
|
|
const NEAR_M = 0.01;
|
|
const HINT = "@at";
|
|
|
|
export type Keyed = { key: string; chainageM: number };
|
|
type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
|
|
|
const lists = new Map<string, Keyed[]>();
|
|
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
|
|
type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
|
const owners = new Map<string, Owner[]>();
|
|
let current: { projectId: string; routeId: number } | null = null;
|
|
|
|
const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${routeId}`;
|
|
|
|
/** 측점 계획(초안 · `planStations`)이 낸 덮는 구조물 id — 관 = `pipe-` 머리(세션 · 자리 이름) 또는 `pipes` 의
|
|
* 서버 관 id(`p8017` · 계산 묶음 측점 열쇠 `pipe:p8017` 과 같은 규칙 · 411-D). */
|
|
export function useStationOwners(
|
|
projectId: string,
|
|
planned: ReadonlyArray<{ chainage_m: number; owners: string[]; role?: StationKeyRole }>,
|
|
pipes: ReadonlyArray<{ id?: string | null }> = [],
|
|
): void {
|
|
const pipeIds = new Set(pipes.map((pipe) => pipe.id));
|
|
owners.set(
|
|
projectId,
|
|
planned.flatMap((station) =>
|
|
station.owners.slice(0, 1).map((id) => ({
|
|
id,
|
|
kind:
|
|
id.startsWith("pipe-") || pipeIds.has(id) ? ("pipe" as const) : ("structure" as const),
|
|
at: station.chainage_m,
|
|
role: station.role ?? "anchor",
|
|
})),
|
|
),
|
|
);
|
|
}
|
|
|
|
/** 구조물 목록(횡단 보기) — 한 자리 구조물의 id. 관 자리는 관 장부가 정함. */
|
|
export function useStructureIds(
|
|
projectId: string,
|
|
structures: ReadonlyArray<{
|
|
structure_id?: string | null;
|
|
chainage_m?: number | null;
|
|
start_m?: number | null;
|
|
end_m?: number | null;
|
|
}>,
|
|
): void {
|
|
const known = (owners.get(projectId) ?? []).filter((owner) => owner.kind === "pipe");
|
|
for (const item of structures) {
|
|
const id = item.structure_id;
|
|
if (!id) continue;
|
|
// 기준 측점 = structure:<id> · 구간형 시작 · 끝 = :start · :end(열쇠 한 벌 · common_util_station_key)
|
|
const marks: Array<[StationKeyRole, number | null | undefined]> = [
|
|
["anchor", item.chainage_m],
|
|
["start", item.start_m],
|
|
["end", item.end_m],
|
|
];
|
|
for (const [role, at] of marks)
|
|
if (typeof at === "number") known.push({ id, kind: "structure", at, role });
|
|
}
|
|
owners.set(projectId, known);
|
|
}
|
|
|
|
const looksLikePipe = (section: CrossSection): boolean =>
|
|
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
|
|
|
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). */
|
|
export function stationKeysOf(
|
|
projectId: string | null,
|
|
sections: ReadonlyArray<CrossSection>,
|
|
intervalM: number | null,
|
|
): Keyed[] {
|
|
const known = projectId ? (owners.get(projectId) ?? []) : [];
|
|
const planned = known.some((owner) => owner.kind === "pipe");
|
|
const keys = sections.map((section) => {
|
|
const at = section.chainage_m;
|
|
let key: string;
|
|
if (section.kind === "irregular") {
|
|
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
|
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
|
else if (looksLikePipe(section)) {
|
|
// 관 계획을 아는 동안(초안) 계획에 없는 관 측점 = 옮겨 간 옛 자리 — 세션 id 를 새로 안 지음(411-E)
|
|
const id = planned ? pipeIdNear(at, projectId) : pipeIdAt(at, projectId);
|
|
key = id ? stationKey("pipe", at, intervalM, id) : stationKey("irregular", at, intervalM);
|
|
} else
|
|
key = section.grid
|
|
? stationKey("regular", at, intervalM)
|
|
: stationKey("irregular", at, intervalM);
|
|
} else key = stationKey(section.kind, at, intervalM);
|
|
return key;
|
|
});
|
|
const chainages = sections.map((section) => section.chainage_m);
|
|
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
|
}
|
|
|
|
type KeyedDetail = Pick<SectionDetailResponse, "cross_sections" | "longitudinal">;
|
|
|
|
/** 측점 간격 — 종단 정책 → 없으면 측점에서 짐작. */
|
|
export function stationIntervalOf(detail: KeyedDetail): number {
|
|
return (
|
|
(
|
|
detail.longitudinal?.profile_alignment as
|
|
{ policy?: { station_interval_m?: number } } | undefined
|
|
)?.policy?.station_interval_m ?? inferStationInterval(detail.cross_sections)
|
|
);
|
|
}
|
|
|
|
/** 상세가 측점을 다 얹었다 — 이 노선의 지금 측점 열쇠를 세움(읽기 · 쓰기가 이 목록으로 옮김). */
|
|
export function useStationKeys(projectId: string, routeId: number, detail: KeyedDetail): void {
|
|
const list = stationKeysOf(projectId, detail.cross_sections, stationIntervalOf(detail));
|
|
lists.set(scopeOf(projectId, routeId), list);
|
|
current = { projectId, routeId };
|
|
}
|
|
|
|
/** 열쇠 목록을 그대로 세움 — 계산 묶음 측점(`PipelineStation.key`)이 정한 열쇠(346-2 4파). */
|
|
export function useStationKeyList(projectId: string, routeId: number, list: Keyed[]): void {
|
|
lists.set(scopeOf(projectId, routeId), list);
|
|
current = { projectId, routeId };
|
|
}
|
|
|
|
/**
|
|
* 지금 상세 측점마다 열쇠 — 세워 둔 목록(그 자리 0.01 m) → 없으면 규칙으로 셈. 관 장부가 이미 새 자리로
|
|
* 옮긴 뒤에도(옮긴 관) 옛 측점은 세울 때의 열쇠로 찾음.
|
|
*/
|
|
export function registeredKeys(
|
|
projectId: string,
|
|
routeId: number,
|
|
sections: ReadonlyArray<CrossSection>,
|
|
intervalM: number | null,
|
|
): string[] {
|
|
const list = lists.get(scopeOf(projectId, routeId));
|
|
const computed = stationKeysOf(projectId, sections, intervalM);
|
|
return sections.map((s, i) => (list && nearest(list, s.chainage_m)?.key) ?? computed[i].key);
|
|
}
|
|
|
|
/** 지금 노선(마지막으로 세운 목록) — 세션 열쇠만 아는 저장소(암 경계 · 암 경사)가 씀. */
|
|
export const currentStationScope = () => current;
|
|
|
|
function nearest(list: Keyed[], chainageM: number): Keyed | undefined {
|
|
let best: Keyed | undefined;
|
|
for (const entry of list) {
|
|
const gap = Math.abs(entry.chainageM - chainageM);
|
|
if (gap < NEAR_M && (!best || gap < Math.abs(best.chainageM - chainageM))) best = entry;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/** 누가거리 → 열쇠(목록이 없거나 그 자리 측점이 없으면 `at:`). */
|
|
export function keyAt(projectId: string, routeId: number, chainageM: number): string {
|
|
const list = lists.get(scopeOf(projectId, routeId));
|
|
return (list && nearest(list, chainageM)?.key) ?? `at:${chainageKey(chainageM)}`;
|
|
}
|
|
|
|
/**
|
|
* 열쇠 → 지금 누가거리(0.01 m). 목록에 있으면 그 측점 · 없으면 쓴 때 자리(`hint` · 옛 열쇠는 그 수)에 지금
|
|
* 측점이 있으면 그 측점 · 목록이 아직 없으면 쓴 때 자리 그대로 · 그 밖 = null(노선 밖).
|
|
*/
|
|
export function chainageOfKey(
|
|
projectId: string,
|
|
routeId: number,
|
|
key: string,
|
|
hint?: number,
|
|
): number | null {
|
|
const list = lists.get(scopeOf(projectId, routeId));
|
|
const hit = list?.find((entry) => entry.key === key);
|
|
if (hit) return Number(chainageKey(hit.chainageM));
|
|
const at = looseChainage(key) ?? hint;
|
|
if (at === undefined) return null;
|
|
if (!list) return Number(chainageKey(at));
|
|
const near = nearest(list, at);
|
|
return near ? Number(chainageKey(near.chainageM)) : null;
|
|
}
|
|
|
|
function readRaw(name: KeyedState, projectId: string, routeId: number): Raw {
|
|
const raw = readState<Raw>(name, projectId, routeId);
|
|
return raw && typeof raw === "object" ? raw : {};
|
|
}
|
|
|
|
function entries<V>(raw: Raw): Array<[string, V, number | undefined]> {
|
|
const hints = raw[HINT] ?? {};
|
|
return Object.entries(raw)
|
|
.filter(([key]) => key !== HINT)
|
|
.map(([key, value]) => [key, value as V, hints[key]]);
|
|
}
|
|
|
|
/**
|
|
* 칸 하나를 누가거리(0.01 m 글) → 값으로 읽음 — 지금 측점에 없는 고침은 뺌(지우지 않음).
|
|
* 옛 열쇠(누가거리 · at:)는 지금 측점 열쇠로 옮겨 다시 씀(목록이 있을 때).
|
|
*/
|
|
export function readKeyed<V>(name: KeyedState, projectId: string, routeId: number): Map<string, V> {
|
|
const raw = readRaw(name, projectId, routeId);
|
|
const out = new Map<string, V>();
|
|
let moved = false;
|
|
for (const [key, value, hint] of entries<V>(raw)) {
|
|
const at = chainageOfKey(projectId, routeId, key, hint);
|
|
if (at === null) continue;
|
|
out.set(chainageKey(at), value);
|
|
if (looseChainage(key) !== null && !keyAt(projectId, routeId, at).startsWith("at:"))
|
|
moved = true;
|
|
}
|
|
if (moved) writeKeyed(name, projectId, routeId, out);
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* 누가거리(0.01 m 글) → 값 묶음을 측점 열쇠로 씀. 지금 측점이 아닌 옛 고침(노선 밖 · 아직 못 읽는 것)은 그대로 둠.
|
|
* `only` 를 주면 그 누가거리들만 바꾸고 나머지 지금 측점 고침도 그대로.
|
|
*/
|
|
export function writeKeyed<V>(
|
|
name: KeyedState,
|
|
projectId: string,
|
|
routeId: number,
|
|
byChainage: ReadonlyMap<string, V>,
|
|
only?: ReadonlyArray<string>,
|
|
): void {
|
|
const raw = readRaw(name, projectId, routeId);
|
|
const next: Raw = { [HINT]: {} };
|
|
const replaced = new Set((only ?? [...byChainage.keys()]).map((at) => chainageKey(Number(at))));
|
|
for (const [key, value, hint] of entries<unknown>(raw)) {
|
|
const at = chainageOfKey(projectId, routeId, key, hint);
|
|
if (at !== null && (only === undefined || replaced.has(chainageKey(at)))) continue;
|
|
next[key] = value;
|
|
if (hint !== undefined) next[HINT]![key] = hint;
|
|
}
|
|
for (const [at, value] of byChainage) {
|
|
if (only !== undefined && !replaced.has(chainageKey(Number(at)))) continue;
|
|
const key = keyAt(projectId, routeId, Number(at));
|
|
next[key] = value;
|
|
next[HINT]![key] = Number(chainageKey(Number(at)));
|
|
}
|
|
writeState(name, next, projectId, routeId);
|
|
}
|
|
|
|
/**
|
|
* 노선이 바뀜(새 측점 목록을 `useStationKeys` 로 세운 뒤) — 옛 노선 고침을 측점 열쇠로 새 노선에 넘김.
|
|
* 넘김 = 카드 선택 넷(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사) · 암 경계 · 암 경사 · 단면 유형 · 측구
|
|
* 방향은 새 지형이 정함 · 노견 확장 · 소단 깊이는 안 넘김. 돌려주는 값 = 노선 밖으로 나간 고침 수(측점 단위).
|
|
* `quiet` = 알림 없이 수만 돌려줌(부르는 쪽이 결과 한 줄에 합칠 때).
|
|
*/
|
|
export function carryStationDrafts(
|
|
projectId: string,
|
|
fromRouteId: number,
|
|
toRouteId: number,
|
|
{ quiet = false }: { quiet?: boolean } = {},
|
|
): number {
|
|
const outside = new Set<string>();
|
|
for (const name of KEYED_STATES) {
|
|
const raw = readRaw(name, projectId, fromRouteId);
|
|
const next: Raw = { [HINT]: {} };
|
|
for (const [key, value] of entries<unknown>(raw)) {
|
|
// 옛 열쇠(누가거리 · at:)는 옛 목록으로 측점 열쇠부터 · 측점 열쇠(id)는 그대로
|
|
const fromAt =
|
|
looseChainage(key) === null ? null : chainageOfKey(projectId, fromRouteId, key);
|
|
const stable = fromAt === null ? key : keyAt(projectId, fromRouteId, fromAt);
|
|
const toAt = chainageOfKey(projectId, toRouteId, stable, looseChainage(stable) ?? undefined);
|
|
const carried = carriedValue(name, value);
|
|
if (toAt === null || stable.startsWith("at:")) {
|
|
if (carried !== null) outside.add(stable);
|
|
continue;
|
|
}
|
|
if (carried === null) continue;
|
|
const toKey = keyAt(projectId, toRouteId, toAt);
|
|
next[toKey] = carried;
|
|
next[HINT]![toKey] = toAt;
|
|
}
|
|
writeState(name, next, projectId, toRouteId);
|
|
}
|
|
if (outside.size && !quiet)
|
|
showToast(`노선 밖 고침 ${outside.size} 건 — 사라진 측점이라 버렸습니다.`, "info");
|
|
return outside.size;
|
|
}
|
|
|
|
/** 잠시 쓰는 새 목록 자리 — 같은 노선 안에서 목록이 갈릴 때. */
|
|
const NEXT_ROUTE = -1;
|
|
|
|
/**
|
|
* 같은 노선에서 노선 모양이 바뀌어 측점 목록이 갈림(346-2 4파 · 상세 갈아끼움) — 지금 목록 고침을 새 목록에
|
|
* 넘기고(넘김 규칙 = `carryStationDrafts`) 새 목록을 세움. 돌려주는 값 = 노선 밖으로 나간 고침 수.
|
|
*/
|
|
export function carryStationKeys(projectId: string, routeId: number, next: Keyed[]): number {
|
|
lists.set(scopeOf(projectId, NEXT_ROUTE), next);
|
|
const outside = carryStationDrafts(projectId, routeId, NEXT_ROUTE);
|
|
for (const name of KEYED_STATES) {
|
|
writeState(name, readRaw(name, projectId, NEXT_ROUTE), projectId, routeId);
|
|
writeState(name, null, projectId, NEXT_ROUTE);
|
|
}
|
|
lists.delete(scopeOf(projectId, NEXT_ROUTE));
|
|
useStationKeyList(projectId, routeId, next);
|
|
return outside;
|
|
}
|
|
|
|
/** 칸마다 넘기는 값 — 넘길 것이 없으면 null. */
|
|
function carriedValue(name: KeyedState, value: unknown): unknown {
|
|
if (CARRY_WHOLE.includes(name)) return value;
|
|
if (name !== "crossdesign" || !value || typeof value !== "object") return null;
|
|
const source = value as Record<string, unknown>;
|
|
const picked = Object.fromEntries(
|
|
CARRY_CHOICE_KEYS.filter((key) => source[key] !== undefined).map((key) => [key, source[key]]),
|
|
);
|
|
return Object.keys(picked).length ? picked : null;
|
|
}
|
|
|
|
/** 노선 저장 때 시설 측점 풀이(`carryChainageStates`). */
|
|
export interface CarryFacilities {
|
|
/** 옛 저장 시설 측점 누가거리 → 새 노선 그 시설 측점 누가거리(없으면 null). */
|
|
movedAt?: (chainageM: number) => number | null | undefined;
|
|
/** 관 · 구조물 열쇠 → 새 노선 누가거리(없으면 null). */
|
|
keyAt?: (key: string) => number | null | undefined;
|
|
}
|
|
|
|
/** 누가거리 열쇠 머리(`b` · 0.01 m 글 · `:역할`) — 조정창 맵 열쇠 꼴(`renameChainageKeys` 와 같음). */
|
|
const CHAINAGE_HEAD = /^(b?)(-?\d+(?:\.\d+)?)(:.*)?$/;
|
|
|
|
/**
|
|
* 노선 저장 뒤(새 노선 측점 열쇠를 세운 뒤) 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고)을 옛 노선
|
|
* → 새 노선 측점으로 옮김 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 ·
|
|
* 못 옮긴 고침은 버림. 돌려주는 값 = 버린 측점 수(「노선 밖 고침」 · 측점 단위).
|
|
* 시설 풀이(`facilities`) = 옛 저장 시설 측점 → 새 노선 그 시설 측점(좌표 짝 · 초안으로 안 따라간 고침) · 관 ·
|
|
* 구조물 열쇠 → 새 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`).
|
|
*/
|
|
export function carryChainageStates(
|
|
projectId: string,
|
|
fromRouteId: number,
|
|
toRouteId: number,
|
|
facilities: CarryFacilities = {},
|
|
): number {
|
|
const facilityAt = facilities.keyAt ?? (() => null);
|
|
const outside = new Set<string>();
|
|
const moveAt = (state: string, text: string): string | null => {
|
|
const moved = facilities.movedAt?.(Number(text));
|
|
if (moved !== null && moved !== undefined) return chainageKey(moved);
|
|
const stable = keyAt(projectId, fromRouteId, Number(text));
|
|
const structural = stable.startsWith("pipe:") || stable.startsWith("structure:");
|
|
// 새 목록에 그 열쇠가 없으면 그 시설의 새 자리 · 없으면 같은 자리 측점(초안 측점 = 저장 측점)
|
|
const toAt =
|
|
stable.startsWith("at:") || (STRUCTURE_STATES.has(state) && !structural)
|
|
? null
|
|
: chainageOfKey(
|
|
projectId,
|
|
toRouteId,
|
|
stable,
|
|
(structural ? facilityAt(stable) : null) ?? Number(text),
|
|
);
|
|
if (toAt === null) outside.add(stable);
|
|
return toAt === null ? null : chainageKey(toAt);
|
|
};
|
|
const rekey = (state: string, cells: Record<string, unknown>): Record<string, unknown> => {
|
|
const next: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(cells)) {
|
|
const head = CHAINAGE_HEAD.exec(key);
|
|
const to = head && moveAt(state, head[2]);
|
|
if (head && to !== null) next[`${head[1]}${to}${head[3] ?? ""}`] = value;
|
|
}
|
|
return next;
|
|
};
|
|
for (const state of Object.keys(ROUTE_CARRY_STATES)) {
|
|
if ((KEYED_STATES as readonly string[]).includes(state)) continue; // 측점 열쇠 칸 = carryStationDrafts
|
|
const name = state as StateName;
|
|
const raw = readState<Record<string, unknown>>(name, projectId, fromRouteId);
|
|
if (!raw || typeof raw !== "object") continue;
|
|
// 연동 = { detached: {누가거리: …}, grade: {…} } 두 겹
|
|
const next =
|
|
state === "revetlink"
|
|
? Object.fromEntries(
|
|
Object.entries(raw).map(([part, cells]) => [
|
|
part,
|
|
cells && typeof cells === "object"
|
|
? rekey(state, cells as Record<string, unknown>)
|
|
: cells,
|
|
]),
|
|
)
|
|
: rekey(state, raw);
|
|
writeState(name, next, projectId, toRouteId);
|
|
}
|
|
return outside.size;
|
|
}
|