Files
Aislo/D03_PlanAnalysis/D03_PlanAnalysis_UI_Terrain.ts
T

95 lines
4.1 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_UI_Terrain.ts
* D 예정노선 받기 이음(PLAN 597 · 요구 463-7) — 페이지에서 뗀 조각: 기준점 꼴 · 둘레 상자 · 받기 지켜보기(서클).
* 상태는 페이지가 쥐고 여기는 인자로만 받음(화면 요소 만들기 없음 · 서클은 부품 그대로).
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
import type { createProgressCircle } from "@ui/ui_template_progress";
import { fetchPlanProgress, type PlanJob } from "./D03_PlanAnalysis_Api";
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
import { lonLatToTm, tmParams } from "./logic/D03_PlanAnalysis_Tm";
const POLL_MS = 1000;
/** 받기가 끊김(서버 다시 켜짐 · 진행을 못 읽음) — [다시 받기]. */
const INTERRUPTED = "끊김(서버 다시 켜짐 등)";
/** 처음 맞춤 = 기준점 둘레(서버 `SITE_RADIUS_M`). */
const SITE_RADIUS_M = 1000;
/** 조용히 받기가 덮는 거리 — 기준점에서 이보다 먼 지점 · 선은 안 받음(위치를 멀리 옮긴 뒤 옛 선 = 거대 받기 막음). */
const COVER_MAX_M = 10000;
/** 기준점(위치 칸 · 등록과 한 칸 `projects.site_*`). */
export interface Site {
lon: number;
lat: number;
address: string | null;
}
export const sleep = (ms: number): Promise<void> => new Promise((done) => setTimeout(done, ms));
export const errorText = (error: unknown): string =>
error instanceof Error ? error.message : String(error);
/** 기준점 둘레 ±1 km(작업 좌표) — 처음 맞춤 · 위치 옮김 · 임도망 맞춤. */
export function aroundSite(epsg: number, at: Site | null): Vertex[] {
const tm = tmParams(epsg);
if (!at || !tm) return [];
const [x, y] = lonLatToTm(tm, at.lon, at.lat);
return [
[x - SITE_RADIUS_M, y - SITE_RADIUS_M],
[x + SITE_RADIUS_M, y + SITE_RADIUS_M],
];
}
/** 경위도 상자 [경도1, 위도1, 경도2, 위도2] → 작업 좌표 두 점(없음 · 띠 모름 = 빈 목록). */
export function boxOfLonLat(epsg: number, bounds: number[] | null | undefined): Vertex[] {
const tm = tmParams(epsg);
if (!tm || !bounds) return [];
return [lonLatToTm(tm, bounds[0], bounds[1]), lonLatToTm(tm, bounds[2], bounds[3])];
}
/** 기준점 둘레 상자 가운데에서 `COVER_MAX_M` 안인 점만 남김(기준점 없음 = 모두). */
export function nearSite(epsg: number, at: Site | null, list: Vertex[]): Vertex[] {
const [lo, hi] = aroundSite(epsg, at);
if (!lo) return list;
return list.filter(
([x, y]) => Math.hypot(x - (lo[0] + hi[0]) / 2, y - (lo[1] + hi[1]) / 2) <= COVER_MAX_M,
);
}
export interface WatchParams {
projectId: string;
alive: () => boolean;
circle: ReturnType<typeof createProgressCircle>;
/** 받는 중 켬끔(페이지 `busy`) 뒤 화면 갱신. */
setBusy: (on: boolean) => void;
}
/** 지형 받기를 끝날 때까지 지켜봄 — 진행 = 지도 귀퉁이 서클 · 끊김(서버 다시 켜짐 = 끝 아닌데 멎음 ·
* 진행을 못 읽음) = 실패 꼴 + [다시 받기]. */
export async function watchTerrain(params: WatchParams): Promise<PlanJob | null> {
const { projectId, alive, circle, setBusy } = params;
setBusy(true);
circle.set(null, "지형");
circle.root.hidden = false;
try {
for (;;) {
if (!alive()) return null;
const job = (await fetchPlanProgress(projectId)).terrain;
circle.set(job.percent / 100, `지형 ${job.message ?? ""}`.trim());
circle.root.title = `지형 받기 ${job.percent}% · ${job.message ?? ""}`;
if (!job.running) {
const cut = !job.error && (job.interrupted || job.percent < 100);
if (job.error) showToast(job.error, "error");
return cut ? { ...job, error: INTERRUPTED } : job;
}
await sleep(POLL_MS);
}
} catch (error) {
showToast(`진행을 읽지 못했습니다 — ${errorText(error)}`, "error");
return { running: false, percent: 0, message: null, error: INTERRUPTED };
} finally {
circle.root.hidden = true;
setBusy(false);
}
}