Files
Aislo/B05_Profile/B05_Profile_UI_Panel_Plan.ts
T
eomsangdonandClaude Opus 5.5 e6a420788f feat(635-5): B05 경위도 ↔ 사업지 좌표 서버 변환 길 · 찾기 이동 정확 · 속성 판 경위도(요구 546)
- 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>
2026-11-01 12:51:03 +09:00

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;
},
};
}