- 받기 끝 = 지형면 · 받은 범위 다 읽은 뒤 겹 판정(동시 읽기 = 옛 범위 판정 뿌리) · 되돌리기 · 다시하기도 영역 확인 - 겹 load(have) — 받은 범위 지문 · 위성 틀 바뀜 = 차례 +1 · 자료 · 받는 중 비움 · 켠 겹 모두 다시 · 옛 차례 답은 자료 · GL 에 안 씀 - 등고 · 세류 = 예정노선 겹 길(layers/sheet) · 도엽 겹 모두 서버가 받은 범위로 자른 사본(화면 상자 · 보관함 지문 걷음) - 서클 = 위치 칸 제목 오른쪽 하나 · 들어간 일 수(지형 받기 · 지형면 · 겹 · 임도망 올리기) · 부품 상자 원에 제목 줄 꼴(inline) 덧 · 지도 귀퉁이 서클 걷음 - 기준점 10 km 밖 점 = 위치 칸 알림 한 줄 - 시험 553_4 서클 줄 = 새 자리로 고침 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
5.6 KiB
TypeScript
141 lines
5.6 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_Terrain.ts
|
|
* D 예정노선 받기 이음(PLAN 597 · 604 · 요구 463-7 · 475 · 476-2) — 페이지에서 뗀 조각: 기준점 꼴 · 둘레 상자 ·
|
|
* 받기 지켜보기 · 진행 서클 하나(위치 칸 제목 오른쪽 · 들어간 일 수 = 지형 받기 · 겹 받기 · 처음 열기 · 임도망 올리기).
|
|
* 상태는 페이지가 쥐고 여기는 인자로만 받음(서클은 부품 그대로).
|
|
* ========================================================================== */
|
|
|
|
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,
|
|
);
|
|
}
|
|
|
|
/** 기준점에서 `COVER_MAX_M` 넘는 점 수 — 위치 칸 알림(조용히 안 받음 475). */
|
|
export const farFromSite = (epsg: number, at: Site | null, list: Vertex[]): number =>
|
|
list.length - nearSite(epsg, at, list).length;
|
|
|
|
/** 진행 서클 하나 · 들어간 일 수(0 = 숨김) — 지형 받기 = 진행률 · 나머지 일 = 돎. */
|
|
export interface WorkCircle {
|
|
/** 일 하나를 셈(끝나면 뺌 · 답은 그대로). */
|
|
track: <T>(job: Promise<T>) => Promise<T>;
|
|
/** 지형 받기 진행(없음 = 끝). */
|
|
terrain: (state: { ratio: number; text: string } | null) => void;
|
|
/** 들어간 일 수(검증 창구). */
|
|
count: () => number;
|
|
}
|
|
|
|
export function createWorkCircle(circle: ReturnType<typeof createProgressCircle>): WorkCircle {
|
|
let count = 0;
|
|
let terrain: { ratio: number; text: string } | null = null;
|
|
const show = (): void => {
|
|
circle.root.hidden = count === 0;
|
|
if (terrain) circle.set(terrain.ratio, "지형");
|
|
else circle.set(null, count > 1 ? `받는 중 ${count}` : "받는 중");
|
|
circle.root.title = terrain
|
|
? `지형 받기 ${Math.round(terrain.ratio * 100)}% · ${terrain.text}`
|
|
: `겹 · 파일 받는 중 ${count}`;
|
|
};
|
|
show();
|
|
return {
|
|
track(job) {
|
|
count += 1;
|
|
show();
|
|
return job.finally(() => {
|
|
count -= 1;
|
|
show();
|
|
});
|
|
},
|
|
terrain(state) {
|
|
terrain = state;
|
|
show();
|
|
},
|
|
count: () => count,
|
|
};
|
|
}
|
|
|
|
export interface WatchParams {
|
|
projectId: string;
|
|
alive: () => boolean;
|
|
work: WorkCircle;
|
|
/** 받는 중 켬끔(페이지 `busy`) 뒤 화면 갱신. */
|
|
setBusy: (on: boolean) => void;
|
|
}
|
|
|
|
/** 지형 받기를 끝날 때까지 지켜봄 — 진행 = 위치 칸 서클 · 끊김(서버 다시 켜짐 = 끝 아닌데 멎음 ·
|
|
* 진행을 못 읽음) = 실패 꼴 + [다시 받기]. */
|
|
export function watchTerrain(params: WatchParams): Promise<PlanJob | null> {
|
|
return params.work.track(watching(params));
|
|
}
|
|
|
|
async function watching(params: WatchParams): Promise<PlanJob | null> {
|
|
const { projectId, alive, work, setBusy } = params;
|
|
setBusy(true);
|
|
work.terrain({ ratio: 0, text: "" });
|
|
try {
|
|
for (;;) {
|
|
if (!alive()) return null;
|
|
const job = (await fetchPlanProgress(projectId)).terrain;
|
|
work.terrain({ ratio: job.percent / 100, text: 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 {
|
|
work.terrain(null);
|
|
setBusy(false);
|
|
}
|
|
}
|