Files
Aislo/B06_Section/B06_Section_Station_Keys.ts
T

371 lines
17 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` — 넘기는 칸 = 지반 유형 · 측구 종류 · 측구 선택 · 사면 쉬프트 · 소단 깊이
* (417 · 서버 `CARRY_INPUT_KEYS` · `ROUTE_CARRY_KEYS`) · 자리 칸(암 경계 · 암 경사 · 2단 경사 · 손 상단측)은 같은
* 자리 측점만(값마다 쓴 때 자리 `@seat` · 요구 275-3) · 사라진 측점의 고침은 버리고 「노선 밖 고침 N 건」.
* · 노선 저장 = `carryChainageStates` — 누가거리 열쇠 칸(옹벽 · 세월 · BOX · 집수 조절 · 측점 폭 · 메모 · 상세 표 ·
* `ROUTE_CARRY_STATES`)도 측점 열쇠로 새 노선 측점에 옮김(구조물 조절 = 관 · 구조물 열쇠만 · 서버와 같음).
* ========================================================================== */
import { readState, stateKey, writeState, type StateName } from "../A00_Common/b_page_state";
import { renameChainageKeys } from "./logic/B06_Section_Station_Swap";
import { afterAnySwap } from "./B06_Section_Swap_Bus";
import { pipeIdAt, pipeIdNear } from "../A00_Common/design/design_pipe_ids";
import { showToast } from "@ui/ui_template_elements";
import { chainageKey, looseChainage, type StationKeyRole } from "@util/common_util_station_key";
import { inferStationInterval } from "@util/common_util_svg";
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
import {
carryChainageStates as carryChainageCells,
carryKeyedStates,
chainageIn,
HINT,
keyIn,
KEYED_STATES,
NEAR_M,
nearestKeyed,
rawEntries,
ROUTE_CARRY_STATES,
SEAT_HINT,
stationKeysFrom,
type CarryFacilities,
type Keyed,
type KeyedState,
type Owner,
type Raw,
} from "./logic/B06_Section_Route_Carry";
// 넘김 규칙 · 칸 목록 = logic 한 벌(서버 Node 입구도 · 472-6 6-B) — 부르는 쪽은 여기서 그대로 받음
export { KEYED_STATES, ROUTE_CARRY_STATES, type CarryFacilities, type Keyed, type KeyedState };
const lists = new Map<string, Keyed[]>();
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
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);
}
/** 측점 목록 → 열쇠(규칙 = `stationKeysFrom` · 관 id = 관 세션 장부). */
export function stationKeysOf(
projectId: string | null,
sections: ReadonlyArray<CrossSection>,
intervalM: number | null,
): Keyed[] {
const known = projectId ? (owners.get(projectId) ?? []) : [];
return stationKeysFrom(sections, intervalM, known, {
near: (at) => pipeIdNear(at, projectId),
at: (at) => pipeIdAt(at, projectId),
});
}
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)
);
}
/** 그대로 세운 목록(계산 묶음 열쇠)이 있는 노선 — 같은 측점이면 주인 규칙으로 다시 세지 않음(415 H1). */
const given = new Set<string>();
/** 상세가 측점을 다 얹었다 — 이 노선의 지금 측점 열쇠를 세움(읽기 · 쓰기가 이 목록으로 옮김). */
export function useStationKeys(projectId: string, routeId: number, detail: KeyedDetail): void {
const scope = scopeOf(projectId, routeId);
const now = lists.get(scope);
const sections = detail.cross_sections;
current = { projectId, routeId };
// 옮긴 관의 주인 자리가 아직 옛 자리여도 묶음 열쇠(pipe:<저장 id>)를 세션 id 로 덮지 않음
const same =
now?.length === sections.length &&
sections.every((section, i) => Math.abs(now[i].chainageM - section.chainage_m) < NEAR_M);
if (given.has(scope) && same) return;
given.delete(scope);
lists.set(scope, stationKeysOf(projectId, sections, stationIntervalOf(detail)));
}
/** 열쇠 목록을 그대로 세움 — 계산 묶음 측점(`PipelineStation.key`)이 정한 열쇠(346-2 4파). */
export function useStationKeyList(projectId: string, routeId: number, list: Keyed[]): void {
lists.set(scopeOf(projectId, routeId), list);
given.add(scopeOf(projectId, routeId));
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 && nearestKeyed(list, s.chainage_m)?.key) ?? computed[i].key);
}
/** 누가거리 → 열쇠(목록이 없거나 그 자리 측점이 없으면 `at:`). */
export function keyAt(projectId: string, routeId: number, chainageM: number): string {
return keyIn(lists.get(scopeOf(projectId, routeId)), chainageM);
}
/**
* 열쇠 → 지금 누가거리(0.01 m). 목록에 있으면 그 측점 · 없으면 쓴 때 자리(`hint` · 옛 열쇠는 그 수)에 지금
* 측점이 있으면 그 측점 · 목록이 아직 없으면 쓴 때 자리 그대로 · 그 밖 = null(노선 밖).
*/
export function chainageOfKey(
projectId: string,
routeId: number,
key: string,
hint?: number,
): number | null {
return chainageIn(lists.get(scopeOf(projectId, routeId)), key, hint);
}
function readRaw(name: KeyedState, projectId: string, routeId: number): Raw {
const raw = readState<Raw>(name, projectId, routeId);
return raw && typeof raw === "object" ? raw : {};
}
/**
* 칸 하나를 누가거리(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 rawEntries<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]: {}, [SEAT_HINT]: {} };
const replaced = new Set((only ?? [...byChainage.keys()]).map((at) => chainageKey(Number(at))));
for (const [key, value, hint, seat] of rawEntries<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;
if (seat) next[SEAT_HINT]![key] = seat;
}
const list = lists.get(scopeOf(projectId, routeId));
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)));
// 쓴 때 자리 — 노선이 바뀌면 자리 칸은 이 자리 그대로인 측점만 따라감(요구 275-3)
const seat = list && nearestKeyed(list, Number(at))?.seat;
if (seat) next[SEAT_HINT]![key] = seat;
}
writeState(name, next, projectId, routeId);
}
/**
* 노선이 바뀜(새 측점 목록을 `useStationKeys` 로 세운 뒤) — 옛 노선 고침을 측점 열쇠로 새 노선에 넘김.
* 넘김 규칙 = `carryKeyedStates`(자리 칸은 같은 자리만 · 요구 275-3). 돌려주는 값 = 노선 밖으로 나간 고침 수(측점 단위).
* `quiet` = 알림 없이 수만 돌려줌(부르는 쪽이 결과 한 줄에 합칠 때).
*/
export function carryStationDrafts(
projectId: string,
fromRouteId: number,
toRouteId: number,
{ quiet = false }: { quiet?: boolean } = {},
): number {
const { states, outside } = carryKeyedStates(
(name) => readRaw(name, projectId, fromRouteId),
lists.get(scopeOf(projectId, fromRouteId)),
lists.get(scopeOf(projectId, toRouteId)),
);
for (const name of KEYED_STATES) writeState(name, states[name], 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;
}
/**
* 노선 저장 뒤(새 노선 측점 열쇠를 세운 뒤) 누가거리 열쇠 칸(`ROUTE_CARRY_STATES` 중 측점 열쇠 칸 빼고)을 옛 노선
* → 새 노선 측점으로 옮김 — 옛 누가거리 → 옛 측점 열쇠 → 새 누가거리. 구조물 조절은 관 · 구조물 열쇠만 ·
* 못 옮긴 고침은 버림. 돌려주는 값 = 버린 측점 수(「노선 밖 고침」 · 측점 단위).
* 시설 풀이(`facilities`) = 옛 저장 시설 측점 → 새 노선 그 시설 측점(좌표 짝 · 초안으로 안 따라간 고침) · 관 ·
* 구조물 열쇠 → 새 누가거리(관 id 규칙이 갈림 — 세션 `pipe-s3` · 계산 줄 `pipe:p8017`).
*/
export function carryChainageStates(
projectId: string,
fromRouteId: number,
toRouteId: number,
facilities: CarryFacilities = {},
): number {
const { states, outside } = carryChainageCells(
(name) =>
name === "kindreset"
? readState<Record<string, unknown>>("kindreset", projectId)
: readState<Record<string, unknown>>(name as StateName, projectId, fromRouteId),
lists.get(scopeOf(projectId, fromRouteId)),
lists.get(scopeOf(projectId, toRouteId)),
facilities,
);
for (const [name, next] of Object.entries(states))
if (name === "kindreset") writeState("kindreset", next, projectId);
else writeState(name as StateName, next, projectId, toRouteId);
return outside.size;
}
/** 노선 바뀐 [저장] 본문의 넘김 입력(472-6 6-B) — 서버 Node `stations.carry` 가 같은 규칙으로 새 노선에 얹음.
* 칸 = 측점 열쇠 칸 다섯 · 누가거리 칸(상단측 뺌 · 요구 274) · 관 종류 바꿈 자리 · 옛 노선 열쇠 목록 · 측점 주인 ·
* 관 세션 장부. */
export function routeCarryInputs(projectId: string, fromRouteId: number): Record<string, unknown> {
const states: Record<string, unknown> = {};
for (const name of KEYED_STATES) states[name] = readRaw(name, projectId, fromRouteId);
for (const state of Object.keys(ROUTE_CARRY_STATES)) {
if (state === "uphill" || (KEYED_STATES as readonly string[]).includes(state)) continue;
const raw = readState<Record<string, unknown>>(state as StateName, projectId, fromRouteId);
if (raw && typeof raw === "object") states[state] = raw;
}
const resets = readState<Record<string, unknown>>("kindreset", projectId);
if (resets) states.kindreset = resets;
return {
states,
from_keys: lists.get(scopeOf(projectId, fromRouteId)) ?? null,
owners: owners.get(projectId) ?? [],
pipe_ids: readState("pipe-ids", projectId) ?? null,
};
}
/**
* 측점이 움직임(갈아끼움 · 옛 0.01 m 글 → 새 글) — 누가거리 칸(조정창 값 · 메모 · 상세 표 등)을 세션에서 바로
* 새 자리로(415-3 · 조정창 값 주인 = 세션 칸 · 472-5 5-D). 칸 = `ROUTE_CARRY_STATES`(측점 열쇠 칸 빼고).
*/
function moveIdleStates(moves: ReadonlyMap<string, string>): void {
if (!moves.size || !current) return;
const { projectId, routeId } = current;
const move = (cells: Record<string, unknown>) =>
Object.fromEntries(renameChainageKeys(new Map(Object.entries(cells)), moves));
// 관 구간값 예약(culvertopt · 설계 칸 아님 = 옮김 목록 짝 밖)도 관 새 자리로(444)
for (const state of [...Object.keys(ROUTE_CARRY_STATES), "culvertopt"]) {
if ((KEYED_STATES as readonly string[]).includes(state)) continue;
const name = state as StateName;
const key = stateKey(name, projectId, routeId);
if (!key) continue;
const raw = readState<Record<string, unknown>>(name, projectId, routeId);
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" ? move(cells as Record<string, unknown>) : cells,
]),
)
: move(raw);
writeState(name, next, projectId, routeId);
}
const resets = readState<Record<string, unknown>>("kindreset", projectId); // 445 M7
if (resets) writeState("kindreset", move(resets), projectId);
}
afterAnySwap((_detail, moves) => moveIdleStates(moves));