/* ============================================================================= * B06_Section_Section_Drafts.ts * 초안 측점 한 벌(272 · 273-2) — 관 · 구조물을 놓거나 옮긴 초안을 공유 상세(횡단 카드 · 유토곡선)에 맞춘다. * * · 옛 자리 구조물 측점(서버가 [저장] 때 다시 맞추기 전) = 숨김 — 쥐고 있다가 초안이 다시 그 자리를 * 심거나 초안이 없어지면 돌려 놓음. * · 새 자리(서버 횡단 없음) = 서버가 미리 떠 준 횡단을 캐시에 얹음(`preview-stations` · 파일 · DB * 안 씀) — [저장] 때 서버가 같은 계산으로 정본에 씀. 한 번 뜬 자리는 기억해 다시 안 물음. * * 심는 자리 규칙 = `plantedChainages`(서버 `resolve_extra_stations` 짝) · 카드 이름 = 서버 측점 이름 규칙 * (`pipe_station_label` · 종류 이름) · 관 그림 = 짝 산식 `common_util_culvert_sets`. * ========================================================================== */ import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { fetchStructures, 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 { attachCulvertSets, buildCulvertSets, registryDefaults, } from "@util/common_util_culvert_sets"; import { pipeStationMarks, plantedChainages, SAME_STATION_M, structureStationMarks, } from "../common_util/common_util_structure_station"; import { previewStructureSections, 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; /** 저장된 관 · 구조물 — 한쪽 초안만 있을 때 심는 자리 셈에 씀 · 저장 뒤(`forgetDraftStations`) 다시 받음. */ const saved = new Map>(); /** 미리 뜬 횡단(`프로젝트:노선:누가거리`) — 못 뜬 자리는 null(다시 안 물음) · 저장 뒤 비움. */ const previews = new Map(); /** 초안이 빼 둔 옛 자리 측점. */ const parked = new WeakMap(); /** 상세마다 마지막 맞춤 번호 — 늦게 끝난 옛 맞춤이 새 맞춤을 덮지 않게. */ const turns = new WeakMap(); /** 저장 · 노선 바뀜 뒤 — 쥔 저장분 · 미리 뜬 횡단을 버림. */ export function forgetDraftStations(projectId: string): void { saved.delete(projectId); for (const key of [...previews.keys()]) if (key.startsWith(`${projectId}:`)) previews.delete(key); } function savedOf(projectId: string): Promise<[Pipes, StructureInstance[]]> { let hit = saved.get(projectId); if (!hit) { hit = Promise.all([ fetchDetailPipePoints(projectId).then((res) => res.pipe_points as Pipes), fetchStructures(projectId).then((res) => res.structures), ]); hit.catch(() => saved.delete(projectId)); saved.set(projectId, hit); } return hit; } async function previewOf( projectId: string, routeId: number, chainages: number[], ): Promise { const key = (at: number) => `${projectId}:${routeId}:${at.toFixed(3)}`; const ask = chainages.filter((at) => !previews.has(key(at))); if (ask.length) { const got = await previewStructureSections(projectId, ask); ask.forEach((at) => previews.set(key(at), got.find((s) => near(s.chainage_m, at)) ?? null)); } return chainages.flatMap((at) => previews.get(key(at)) ?? []); } /** 미리 뜬 횡단에 카드 이름 · 관 그림 — 서버가 [저장] 때 붙이는 것과 같은 규칙. */ function decorate( fresh: CrossSection[], pipes: Pipes, structures: StructureInstance[], types: StructureType[], ): void { const registry = registryDefaults(types); const byId = new Map(types.map((type) => [type.type_id, type])); const pipeLabel = (pipe: Pipes[number]): string | null => { const facility = pipe.facility ?? "pipe"; if (facility !== "pipe") return byId.get(facility)?.name ?? facility; const values = { ...registry.pipe, ...pipe.options }; return `${values.pipe_kind} D${Math.trunc(Number(values.pipe_diameter_mm))}`; }; const labels: Array<[number, string]> = [ ...pipes.flatMap((pipe) => { const label = pipeLabel(pipe); return label ? pipeStationMarks(pipe).map((m): [number, string] => [m.chainage_m, label]) : []; }), ...structures.flatMap((structure) => { const type = byId.get(structure.type_id); const label = type?.name ?? structure.type_id; return structureStationMarks(structure, type).map((m): [number, string] => [ m.chainage_m, label, ]); }), ]; for (const section of fresh) { section.preview = true; const hit = labels.find(([at]) => near(at, section.chainage_m)); if (hit) section.structure = hit[1]; } const own = pipes.filter((pipe) => fresh.some((section) => near(section.chainage_m, pipe.chainage_m)), ); attachCulvertSets( fresh as unknown as Array>, buildCulvertSets(own, registry), ); } /** * 초안에 맞춰 상세의 횡단 목록을 고친다 — 옛 자리 숨김 · 새 자리 미리 뜬 횡단 얹음. 초안이 없으면 * 서버 목록 그대로(빼 둔 것 돌려 놓음). 미리 뜨기를 못 하면(샘플링 조건 없음 · 끊김) 새 자리는 * 종전처럼 [저장] 뒤에 섬. */ 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[] = []; let fresh: CrossSection[] = []; if (pipesDraft || structuresDraft) { const inputs = await Promise.all([ pipesDraft ?? savedOf(projectId).then(([pipes]) => pipes), structuresDraft ?? savedOf(projectId).then(([, structures]) => structures), fetchStructureTypes(true), ]).catch(() => null); if (inputs) { const [pipes, structures, types] = inputs; const planted = plantedChainages(pipes, structures, types); const isStale = (section: CrossSection): boolean => section.kind === "irregular" && !section.grid && planted.every((at) => !near(at, section.chainage_m)); stale = all.filter(isStale); live = all.filter((section) => !isStale(section)); const missing = planted.filter( (at, index) => planted.findIndex((other) => near(other, at)) === index && live.every((section) => !near(section.chainage_m, at)), ); fresh = missing.length ? await previewOf(projectId, routeId, missing).catch(() => []) : []; if (fresh.length) decorate(fresh, pipes, structures, types); } } if (turns.get(detail) !== turn) return; // 더 새 맞춤이 돎 — 그쪽이 얹음 detail.cross_sections.splice( 0, detail.cross_sections.length, ...[...live, ...fresh].sort(byChainage), ); parked.set(detail, stale); }