- POST /api/projects/{id}/route/coords — 작업 좌표계(resolve_project_crs · WKT 그대로) pyproj 묶음 변환
- 찾기 이동 = 서버 변환 좌표 · 바탕 tm_grid 늘려 쓰기(ponytail) 걷음
- 키트 lonLat = 노드 고를 때 한 번 묻기 · 같은 점 캐시
- test_635_3 찾기 줄 = 서버 변환 부름으로 고침(옛 tm_grid 부름 걷힘)
- 실측 검증_sub6 B: 50.4 km · 181.2 km 찾기 이동 어긋남 0.000 m · 경위도 = pyproj
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
87 lines
3.3 KiB
TypeScript
87 lines
3.3 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_Panel_Plan.ts
|
|
* 종단 보기 왼쪽 「위치」 · 「그리기」 칸 자리(PLAN 635-3 · 요구 546) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
|
|
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1 · 2) · 편집기가 `fillPlanPanel` 로 채움.
|
|
* 늦게 온 쪽이 붙임 · 새 칸 자리(페이지 다시 세움) = 떠난 편집기 칸(문서 밖) 버림 · 비면 자리 숨김.
|
|
* 좌표 변환(635-5) = 서버(`POST /route/coords` · 사업지 좌표계 pyproj) — 찾기 이동 · 속성 판 경위도 때만 부름.
|
|
* ========================================================================== */
|
|
|
|
import { registerPanelSection } from "../A00_Common/design/design_panel";
|
|
import { requestJson } from "./B05_Profile_Api_Replan";
|
|
|
|
type Vertex = [number, number];
|
|
|
|
type Slot = "site" | "draw";
|
|
const holders: Partial<Record<Slot, HTMLElement>> = {};
|
|
const cards: Partial<Record<Slot, HTMLElement | null>> = {};
|
|
|
|
const attach = (slot: Slot): void => {
|
|
const holder = holders[slot];
|
|
if (!holder) return;
|
|
const card = cards[slot];
|
|
holder.replaceChildren(...(card ? [card] : []));
|
|
holder.hidden = !card;
|
|
};
|
|
|
|
for (const [slot, order, title] of [
|
|
["site", 1, "위치"],
|
|
["draw", 2, "그리기"],
|
|
] as const)
|
|
registerPanelSection({
|
|
id: `plan-${slot}`,
|
|
title,
|
|
views: ["profile"],
|
|
order,
|
|
build: () => {
|
|
const holder = document.createElement("div");
|
|
holder.className = "b05-route__plan-slot";
|
|
holders[slot] = holder;
|
|
if (!cards[slot]?.isConnected) cards[slot] = null; // 떠난 편집기 칸
|
|
attach(slot);
|
|
return holder;
|
|
},
|
|
});
|
|
|
|
/** 키트 칸을 앉힘(새 편집기가 갈음). */
|
|
export function fillPlanPanel(next: Record<Slot, HTMLElement | null>): void {
|
|
for (const slot of ["site", "draw"] as const) {
|
|
cards[slot] = next[slot];
|
|
attach(slot);
|
|
}
|
|
}
|
|
|
|
export interface PlanCoords {
|
|
/** 경위도 → 사업지 [x, y] m(못 받으면 null). */
|
|
toPlan: (lon: number, lat: number) => Promise<Vertex | null>;
|
|
/** 사업지 → 경위도 — 받은 점이면 바로 · 처음이면 null 주고 한 번 물음(오면 `onLonLat`). */
|
|
lonLat: (x: number, y: number) => Vertex | null;
|
|
}
|
|
|
|
/** 경위도 ↔ 사업지 좌표 서버 변환 · 같은 점은 캐시(끌기 중엔 안 부름 — 판은 고름 · 놓음 때만 그림). */
|
|
export function createPlanCoords(projectId: string, onLonLat: () => void): PlanCoords {
|
|
const ask = (to: "plan" | "lonlat", point: Vertex): Promise<Vertex | null> =>
|
|
requestJson<{ points: Vertex[] }>(
|
|
`/projects/${projectId}/route/coords`,
|
|
{ method: "POST", body: JSON.stringify({ to, points: [point] }) },
|
|
15_000,
|
|
).then(
|
|
(answer) => answer.points[0] ?? null,
|
|
() => null,
|
|
);
|
|
const known = new Map<string, Vertex | null>();
|
|
return {
|
|
toPlan: (lon, lat) => ask("plan", [lon, lat]),
|
|
lonLat(x, y) {
|
|
const key = `${x},${y}`;
|
|
if (known.has(key)) return known.get(key) ?? null;
|
|
known.set(key, null); // 묻는 중
|
|
void ask("lonlat", [x, y]).then((at) => {
|
|
if (at) known.set(key, at);
|
|
else known.delete(key); // 실패 = 다음 고름 때 다시
|
|
if (at) onLonLat();
|
|
});
|
|
return null;
|
|
},
|
|
};
|
|
}
|