/* ============================================================================= * B06_Section_Section_Drafts.ts * 초안 측점 한 벌(272 · 273-2) — 관 · 구조물을 놓거나 옮긴 초안을 공유 상세(횡단 카드 · 유토곡선)에 맞춘다. * * · 옛 자리 구조물 측점(서버가 [저장] 때 다시 맞추기 전) = 숨김 — 쥐고 있다가 초안이 다시 그 자리를 * 심거나 초안이 없어지면 돌려 놓음. * · 새 자리(서버 횡단 없음) = 계산 묶음 측점 갈아끼움이 얹음(브라우저 계산 줄 하나 · 415 M4) — 여기는 안 물음. * * 심는 자리 · 카드 이름 = 측점 계획 한 벌(`planStations` · 서버 `resolve_extra_stations` 짝 · 322-3) · * 그림 자료 = 형상 얹기 한 벌(`withDraftShapes` → `attachShapes` · 서버 `attach_shapes` 짝 · 323-4). 일반 측점 * 자리(No.N+0 · BP · EP)에 새로 놓은 구조물도 그 자리 구조물 없는 사본에 이름 · 형상(219 흠 · 322-3). * ========================================================================== */ import { forgetPageData, pagePipes, pageStructures } from "../A00_Common/design/design_page_data"; import { fetchStructureTypes, readPendingStructures, type StructureInstance, type StructureType, } from "../B05_Profile/B05_Profile_Api_Structures"; import { readPendingPipes, type PendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft"; import { readPlusDraft } from "../B05_Profile/B05_Profile_Api_PlusStations"; import { registryDefaults } from "@util/common_util_culvert_sets"; import { attachShapes } from "@util/common_util_structure_attach"; import { pipeStationMarks, planStations, SAME_STATION_M, structureStationMarks, type PlannedStation, } from "../common_util/common_util_structure_station"; import { useStationKeys, useStationOwners } from "./B06_Section_Station_Keys"; import { type CrossSection, type SectionDetailResponse } from "./B06_Section_Api_Fetch"; type Pipes = PendingPipes; const near = (a: number, b: number): boolean => Math.abs(a - b) < SAME_STATION_M; const byChainage = (a: { chainage_m: number }, b: { chainage_m: number }) => a.chainage_m - b.chainage_m; /** 초안이 빼 둔 옛 자리 측점. */ const parked = new WeakMap(); /** 상세마다 마지막 맞춤 번호 — 늦게 끝난 옛 맞춤이 새 맞춤을 덮지 않게. */ const turns = new WeakMap(); /** 상세가 갈아끼워짐 — 옛 상세에 돌던 맞춤은 측점 열쇠 목록을 옛 측점으로 덮지 않게 끝냄(415 H1). */ export function supersedeDraftStations(detail: SectionDetailResponse): void { turns.set(detail, (turns.get(detail) ?? 0) + 1); } /** 저장 · 노선 바뀜 뒤 — 페이지 저장분(자료 줄 · 386-2)을 버림. */ export function forgetDraftStations(projectId: string): void { forgetPageData(projectId); } /** 저장된 관 · 구조물 — 한쪽 초안만 있을 때 심는 자리 셈에 씀(페이지 자료 줄 한 번 · 저장 뒤 다시). */ const savedOf = (projectId: string): Promise<[Pipes, StructureInstance[]]> => Promise.all([ pagePipes(projectId).then((res) => res.pipe_points as Pipes), pageStructures(projectId).then((res) => res.structures), ]); /** 일반 측점 자리(grid · 272)의 「구조물 없는 꼴」(217) — 저장분에서 시설 · 이름만 뺀 사본(한 벌을 다시 씀 · * 지형 쪽 고친 값은 그 측점 것 그대로) · 새 종류는 그 위에 얹음(`decorate`) · 측점은 남으므로 숨기지 않음. */ const bares = new WeakMap(); function bareOf(section: CrossSection): CrossSection { let copy = bares.get(section); if (!copy) { copy = { ...section, preview: true }; const record = copy as unknown as Record; for (const key of ["culvert", "ford", "box", "ford_pavement", "structure"]) delete record[key]; bares.set(section, copy); } return copy; } /** 구조물 없는 사본에 카드 이름(그림 자료는 `withDraftShapes` 가 모든 측점에 한 번). */ function decorate(fresh: CrossSection[], planned: readonly PlannedStation[]): void { for (const section of fresh) { section.preview = true; // 카드 이름 = 측점 계획 이름(322-3 · 같은 m 는 관이 먼저) const hit = planned.find((station) => near(station.chainage_m, section.chainage_m)); if (hit) section.structure = hit.title; else delete section.structure; // 구조물이 다 빠진 일반 측점 자리(217) } } /** 현황 = 종류마다 초안 ?? 저장분 + 종류 표 — 측점 계획 · 형상 얹기가 같은 것(못 받으면 null). */ function statusOf( projectId: string, pipesDraft: Pipes | null, structuresDraft: StructureInstance[] | null, ): Promise<[Pipes, StructureInstance[], StructureType[]] | null> { return Promise.all([ pipesDraft ?? savedOf(projectId).then(([pipes]) => pipes), structuresDraft ?? savedOf(projectId).then(([, structures]) => structures), fetchStructureTypes(true), ]).catch(() => null); } /** 초안으로 그림 자료를 얹은 상세 — 초안이 없어지면 저장분으로 한 번 다시 얹음. */ const drafted = new WeakSet(); /** * 모든 측점 그림 자료 = 형상 얹기 한 벌(323-4 · 서버 `attach_shapes` 짝) — 현황 = 초안 ?? 저장분 · * 저장 전 그림 = 저장 뒤 그림. 초안이 없고 앞서 초안으로 얹은 적도 없으면 서버가 얹은 그대로. */ export async function withDraftShapes( detail: SectionDetailResponse, projectId: string, ): Promise { const pipesDraft = readPendingPipes(projectId); const structuresDraft = readPendingStructures(projectId); const drafts = !!(pipesDraft || structuresDraft); if (!drafts && !drafted.has(detail)) return; const inputs = await statusOf(projectId, pipesDraft, structuresDraft); if (!inputs) return; attachShapes(detail.cross_sections as unknown as Array>, ...inputs); if (drafts) drafted.add(detail); else drafted.delete(detail); } /** * 초안에 맞춰 상세의 횡단 목록을 고친다 — 옛 자리 숨김 · 종류 바뀐 자리 구조물 없는 사본. 초안이 없으면 * 서버 목록 그대로(빼 둔 것 돌려 놓음). 새 자리는 계산 묶음 측점 갈아끼움이 얹음(415 M4). */ export async function withDraftStations( detail: SectionDetailResponse, projectId: string, routeId: number, ): Promise { const turn = (turns.get(detail) ?? 0) + 1; turns.set(detail, turn); const pipesDraft = readPendingPipes(projectId); const structuresDraft = readPendingStructures(projectId); const all = [ ...detail.cross_sections.filter((section) => !section.preview), ...(parked.get(detail) ?? []), ].sort(byChainage); let live = all; let stale: CrossSection[] = []; // 추가 측점 초안(368-2)도 측점 계획을 바꿈 — 계획은 그 초안을 스스로 읽음 if (!(pipesDraft || structuresDraft || readPlusDraft(projectId))) { // 초안 없음 = 저장분 계획으로 열쇠 주인만 — 관 = 저장 id(계산 묶음 열쇠와 같음 · 첫 옮김도 열쇠째 · 415 H1) const inputs = await statusOf(projectId, null, null); if (inputs) { const [pipes, structures, types] = inputs; useStationOwners( projectId, planStations(pipes, structures, types, registryDefaults(types)), pipes, ); } } else { const inputs = await statusOf(projectId, pipesDraft, structuresDraft); if (inputs) { const [pipes, structures, types] = inputs; const planned = planStations(pipes, structures, types, registryDefaults(types)); useStationOwners(projectId, planned, pipes); // 측점 열쇠 = 덮는 구조물 id(344-3) · 관 = pipe: const planted = planned.map((station) => station.chainage_m); // 구조물 측점에서 종류를 바꿈 = 「구조물 없는 상태」부터 새 종류로(요구 213) — 저장분 횡단은 빼 두고 // 구조물 없는 사본을 씀 · 저장분 종류로 되돌리면 빼 둔 횡단이 돌아옴 const [oldPipes, oldStructures] = await savedOf(projectId).catch( (): [Pipes, StructureInstance[]] => [[], []], ); const byType = new Map(types.map((type) => [type.type_id, type])); const kindsAt = (ps: Pipes, ss: StructureInstance[], at: number): string => [ ...ps .filter((pipe) => pipeStationMarks(pipe).some((m) => near(m.chainage_m, at))) .map((pipe) => `p:${pipe.facility ?? "pipe"}`), ...ss .filter((s) => structureStationMarks(s, byType.get(s.type_id)).some((m) => near(m.chainage_m, at)), ) .map((s) => `s:${s.type_id}`), ] .sort() .join(","); const replaced = (section: CrossSection): boolean => { const was = kindsAt(oldPipes, oldStructures, section.chainage_m); return was !== "" && was !== kindsAt(pipes, structures, section.chainage_m); }; // 일반 측점 자리(grid · 272)는 측점이 남음 — 종류가 바뀌거나 빠지면 구조물 없는 사본에 새 종류(217) const isStale = (section: CrossSection): boolean => section.kind === "irregular" && (section.grid ? replaced(section) : planted.every((at) => !near(at, section.chainage_m)) || replaced(section)); // 일반 측점 자리(No.N+0 · BP · EP)에 새로 놓은 구조물 = 그 자리 구조물 없는 사본에 이름 · 형상(219) — // 구조물 측점(irregular)은 위 종류 대조가 맡음 · 옛 것은 빼 둠(초안이 없어지면 돌아옴) const retitled = all.filter( (section) => section.kind !== "irregular" && planned.some((station) => near(station.chainage_m, section.chainage_m)), ); stale = [...all.filter(isStale), ...retitled]; const bare = [ // 종류만 바뀐 자리(측점이 남음) — 시설이 떠난 자리는 숨김만 ...all.filter( (section) => isStale(section) && replaced(section) && (section.grid || planted.some((at) => near(at, section.chainage_m))), ), ...retitled, ].map(bareOf); live = [...all.filter((section) => !stale.includes(section)), ...bare]; if (bare.length) decorate(bare, planned); // 계산 묶음 갈아끼움이 얹은 새 자리 = 카드 이름만(415 M4) for (const section of live) if (section.kind === "irregular" && !section.structure) { const hit = planned.find((station) => near(station.chainage_m, section.chainage_m)); if (hit) section.structure = hit.title; } } } if (turns.get(detail) !== turn) return; // 더 새 맞춤이 돎 — 그쪽이 얹음 detail.cross_sections.splice(0, detail.cross_sections.length, ...live.sort(byChainage)); parked.set(detail, stale); useStationKeys(projectId, routeId, detail); // 세션 횡단 초안 = 이 측점 목록의 열쇠(344-3) }