- 편집 모달 「분석 지형: 라이다 · 수치지형도」 고르기 · 라이다와 수치지형도 서피스가 둘 다 있을 때만 보임 · 기본 라이다 · 값은 화면 상태 pref(routeedit-terrain) - 고르면 등고선 · 구간 기울기 · 거리 재기 지반고 · 횡단 미리보기 · 종단 띠를 고른 지형으로 다시 그림 · 노선 추천도 고른 지형으로 요청 - 서버 통로(elevations · cross-preview · profile-preview · initial)가 terrain(las · sheet) 을 받아 sampler 를 고름 · GET route/terrain-sources 새 길 - 확정 params · [확인] 재계산 · 종 · 횡단 · 3D 정본은 안 바뀜 - 지표면 허용 목록에 sheet_* 여섯 넣음 — 54-22 뒤 LAS 없는 프로젝트 sheet_laplace 가 막혀 있던 길도 풀림 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
117 lines
5.1 KiB
TypeScript
117 lines
5.1 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 {
|
|
analysisTerrain,
|
|
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, analysisTerrain(projectId)]);
|
|
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();
|
|
},
|
|
};
|
|
}
|