- 알약 = 예상노선 추적(종단 없음) · 영선 · 토공 최소 · 기울기 최소 · 비중 계산(넷은 종단 늘 반영) · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소 화면에서 뺌 - 「종단 반영」 켬/끔 단추 없앰(추천 창 · 종단 띠) — 띠 · [확인]은 지금 추천 갈래로 종단 여부 - ✕ 없애고 [적용] · [닫기]를 제목줄 오른쪽 위로 · 옵션 칸은 지도 왼쪽 위 작은 겹창 · 창 폭 넓힘 - 적용한 추천 갈래가 있으면 창을 다시 열 때 그 갈래가 골라진 채 - 도움말: 종단 반영 = 측점마다 횡단을 보고 정한 종단(적분값 아님) - 균형 종단 단면적에 곡선부 확폭(B05 측점 생성과 같은 셈) — 빼서 확정 뒤 성토가 1.7배 벌어졌음(노선 267 같은 계획고: 성 10,102 → 17,819 · 본 설계 16,140) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019EMfGERKUg5nMMNQsyVZ2j
116 lines
5.0 KiB
TypeScript
116 lines
5.0 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_ProfileStrip.ts
|
|
* 계획노선 편집 창 지도 아래 **종단 띠**(PLAN 75-1) — 지금 노선 전체 연장의 원지반선 · 계획선 ·
|
|
* 측점 눈금 한 줄.
|
|
*
|
|
* 보여 주기만 한다(PLAN 77-5) — 종단 여부는 지금 추천 갈래가 정한다(`withProfile`). 종단 갈래 =
|
|
* 계획선은 지금 노드로 서버가 다시 낸 균형 종단(`/route/profile-preview` — [확인] 체인과 같은 셈).
|
|
* 예상노선 추적 = 지반선만(계획고 = 지반고 · `/route/elevations`). [확인]도 같은 규칙을 탄다
|
|
* (`_Apply` 의 `balanced_profile`).
|
|
*
|
|
* 갱신은 노드를 놓을 때마다(끌기 중 아님) · 최신 요청만 채택(seq) · 기다리는 동안 옛 그림은 흐리게.
|
|
* 계산 자리 = 서버 단독 — 여기는 받은 값을 그리기만 한다.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
fetchProfilePreview,
|
|
fetchRouteElevations,
|
|
type RouteReplanVertex,
|
|
} from "./B05_Profile_Api_Replan";
|
|
import { parseBalance, profileSvg } from "./B05_Profile_UI_RouteEdit_Recommend_Profile";
|
|
import { lengthOf, pointsAt, stationChainages } from "./B05_Profile_UI_RouteEdit_Slope";
|
|
|
|
type Vertex = [number, number];
|
|
|
|
/** 지반선만 그릴 때 측점 간격(m) — 균형 종단(서버 `STATION_M`)과 같음. */
|
|
const STATION_M = 20;
|
|
|
|
export interface ProfileStripParams {
|
|
root: HTMLElement;
|
|
projectId: string;
|
|
/** 지금 추천 갈래가 종단을 반영하는가(예상노선 추적만 아님). */
|
|
withProfile: () => boolean;
|
|
/** 지금 편집값 — 노드(서버로) · 그려진 노선(지반선 측점) · 지금 추천 제약. */
|
|
now: () => { vertices: RouteReplanVertex[]; line: ReadonlyArray<Vertex>; criteria: unknown };
|
|
}
|
|
|
|
export interface ProfileStrip {
|
|
/** 노선이 바뀌었다(놓음 · 되돌리기 · 추천 적용) — 다시 묻는다. */
|
|
refresh: () => void;
|
|
/** [확인] 직전 — 지금 노선으로 띠를 맞추고 응답을 기다려, 띠에 보인 계획선이 균형 종단인지
|
|
* 돌려준다(보인 것 = 적용된 것). 띠가 못 그렸으면 갈래 값. */
|
|
settle: () => Promise<boolean>;
|
|
}
|
|
|
|
export function createProfileStrip(params: ProfileStripParams): ProfileStrip {
|
|
const { root, projectId } = params;
|
|
const note = root.querySelector<HTMLElement>("[data-strip-note]")!;
|
|
const graph = root.querySelector<HTMLElement>("[data-strip-graph]")!;
|
|
let seq = 0;
|
|
/** 마지막으로 그렸거나 묻는 중인 값 — 같은 노선 · 같은 갈래 종단이면 다시 묻지 않는다. */
|
|
let lastKey = "";
|
|
/** 마지막 요청(끝날 때까지) · 띠에 그려진 계획선이 균형 종단인가(못 그렸으면 null). */
|
|
let pending: Promise<void> = Promise.resolve();
|
|
let shownOn: boolean | null = null;
|
|
|
|
async function groundOnly(line: ReadonlyArray<Vertex>) {
|
|
const stationsM = stationChainages(lengthOf(line), STATION_M);
|
|
const z = await fetchRouteElevations(projectId, pointsAt(line, stationsM));
|
|
const known = stationsM.flatMap((s, i) => (z[i] === null ? [] : [[s, z[i]!] as const]));
|
|
if (known.length < 2) throw new Error("노선 위 지반고를 읽지 못했습니다.");
|
|
return {
|
|
stationsM: known.map(([s]) => s),
|
|
groundM: known.map(([, h]) => h),
|
|
planM: [] as number[],
|
|
};
|
|
}
|
|
|
|
function refresh(): void {
|
|
const on = params.withProfile();
|
|
const { vertices, line, criteria } = params.now();
|
|
const key = JSON.stringify([on, vertices, criteria]);
|
|
if (vertices.length < 2 || line.length < 2 || key === lastKey) return;
|
|
lastKey = key;
|
|
pending = draw(on, vertices, line, criteria);
|
|
}
|
|
|
|
async function draw(
|
|
on: boolean,
|
|
vertices: RouteReplanVertex[],
|
|
line: ReadonlyArray<Vertex>,
|
|
criteria: unknown,
|
|
): Promise<void> {
|
|
const ticket = ++seq;
|
|
root.classList.add("is-waiting");
|
|
note.textContent = "종단을 읽는 중…";
|
|
try {
|
|
const profile = on
|
|
? parseBalance(await fetchProfilePreview(projectId, vertices, criteria))
|
|
: await groundOnly(line);
|
|
if (ticket !== seq || !root.isConnected) return; // 그 사이 노선이 또 바뀌었다.
|
|
if (!profile) throw new Error("균형 종단을 세우지 못했습니다.");
|
|
const total = profile.stationsM[profile.stationsM.length - 1];
|
|
graph.innerHTML = profileSvg(profile, true);
|
|
shownOn = on;
|
|
note.textContent = `${on ? "원지반선 · 계획 종단선" : "원지반선(계획고 = 지반고)"} · 연장 ${total.toFixed(1)}m`;
|
|
} catch (error) {
|
|
if (ticket !== seq) return;
|
|
lastKey = ""; // 실패 — 다음 번에 다시 묻는다.
|
|
shownOn = null;
|
|
graph.innerHTML = "";
|
|
note.textContent = error instanceof Error ? error.message : "종단을 읽지 못했습니다.";
|
|
} finally {
|
|
if (ticket === seq) root.classList.remove("is-waiting");
|
|
}
|
|
}
|
|
|
|
return {
|
|
refresh,
|
|
settle: async () => {
|
|
refresh();
|
|
await pending;
|
|
return shownOn ?? params.withProfile();
|
|
},
|
|
};
|
|
}
|