Files
Aislo/B05_wf2_Route/B05_wf2_Route_UI_Profile_Panel.ts
T
2026-07-19 11:36:25 +09:00

112 lines
3.5 KiB
TypeScript

import type {
LongitudinalSection,
SectionDetailResponse,
} from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_Api_Fetch";
import {
createLongitudinalProfile,
longitudinalMinimumWidth,
} from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View";
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css";
const COLLAPSED_KEY = "b05-route-profile-collapsed";
const HORIZONTAL_SCROLLBAR_HEIGHT = 16;
function normalizedLongitudinal(data: LongitudinalSection): LongitudinalSection {
return {
...data,
samples: data.samples.map((sample) => ({
...sample,
elevation_m: sample.elevation_m ?? sample.z ?? null,
})),
};
}
export function createRouteProfilePanel(onSelectStation: (stationId: string) => void) {
const root = document.createElement("section");
root.className = "b05-route-profile";
const panelHandle = createWorkflowPanelHandle("bottom");
const toggle = panelHandle.root;
const body = document.createElement("div");
body.className = "b05-route-profile__body";
const empty = document.createElement("p");
empty.className = "b05-route-profile__empty";
empty.textContent = "최적 경로를 계산하면 종단면도가 표시됩니다.";
body.append(empty);
root.append(toggle, body);
let detail: SectionDetailResponse | null = null;
let selectedStationId: string | null = null;
let stationInterval: number | undefined;
let resizeTimer = 0;
let lastWidth = 0;
let lastHeight = 0;
function draw(): void {
if (!detail || body.clientWidth <= 0 || body.clientHeight <= 0) return;
const availableWidth = Math.max(1, body.clientWidth - 30);
const height = Math.max(1, body.clientHeight - HORIZONTAL_SCROLLBAR_HEIGHT);
const minimumWidth = longitudinalMinimumWidth(detail.longitudinal, stationInterval);
const width = Math.max(availableWidth, minimumWidth);
lastWidth = body.clientWidth;
lastHeight = height;
body.replaceChildren(
createLongitudinalProfile(
normalizedLongitudinal(detail.longitudinal),
selectedStationId,
1,
undefined,
onSelectStation,
stationInterval,
width,
height,
minimumWidth,
),
);
}
const resizeObserver = new ResizeObserver(() => {
if (
body.clientWidth <= 0 ||
body.clientHeight <= 0 ||
(Math.abs(body.clientWidth - lastWidth) < 1 && Math.abs(body.clientHeight - lastHeight) < 1)
)
return;
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(draw, 150);
});
resizeObserver.observe(body);
function setCollapsed(collapsed: boolean): void {
root.classList.toggle("is-collapsed", collapsed);
panelHandle.setOpen(!collapsed);
sessionStorage.setItem(COLLAPSED_KEY, String(collapsed));
if (!collapsed) requestAnimationFrame(draw);
}
toggle.addEventListener("click", () => setCollapsed(!root.classList.contains("is-collapsed")));
setCollapsed(sessionStorage.getItem(COLLAPSED_KEY) === "true");
return {
root,
render(nextDetail: SectionDetailResponse, nextStationInterval?: number) {
detail = nextDetail;
stationInterval = nextStationInterval;
draw();
requestAnimationFrame(draw);
},
setSelectedStation(stationId: string | null) {
selectedStationId = stationId;
draw();
},
clear() {
detail = null;
selectedStationId = null;
body.replaceChildren(empty);
},
dispose() {
window.clearTimeout(resizeTimer);
resizeObserver.disconnect();
},
};
}