표고 체계 정비·설정 파일 단순화·선형 클리핑·시연 준비

- 타원체고 기준 자동 변환: route.json ellipsoidal_height 선언 시 드론 고도(CSV 정표고)를
  영상 메타(djmd 6.6 타원체고)와의 차(지오이드고 자동 산출)만큼 보정, 세그먼트 전환에도 유지
- KML 표고 파이프라인: 좌표 고도(z) 사용(POI/측점), *_byDem.kml 자동 생성(DEM 조회·높이 속성 포함)
  + 로더의 _byDem 우선 규칙, 선형중심선(LineString) 로딩·경로표고 앵커
- 설정 파일: camera.json/display.json/route.json 단순 이름 인식, 화면표시 옵션 PC 저장/자동 적용,
  초기화 버튼을 환경설정 파일 기준 복원으로 변경, 저장 파일명 단순화
- 선형 표시: 표출된 마지막 측점까지 클립(경계 세그먼트 절단 + 관통 현 처리 — 구간 실종 수정)
- 스테이션바: 시점(0k000) 마크(실통과 위치), 노선밖 프레임 통과 오인 방지, 커서 배지 1m 단위,
  배너 연장/소요 실측값(측점 체이니지·영상 길이 합) 우선
- 세그먼트 전환 라벨 깜빡임 구조 수정(캐시 초기화·최근접 키 조회·이중 재계산 제거) +
  사전계산 프레임 불변값 호이스팅(재생 지연 해소)
- UI: 슬라이더/값 입력칸 휠 조절, 패널 버튼 위치 정렬, 저장 버튼 중복 제거, dev_mode(route.json)로
  영상제어 UI 표시 제어, 지오이드 최대 200, 이격거리 측점 동반 적용
- 시연: start-demo.bat + docs/DEMO.md
- docs/history: 작업 이력 30건

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 20:37:46 +09:00
co-authored by Claude Fable 5
parent 84f4921826
commit 7d51ba9945
53 changed files with 1708 additions and 104 deletions
+28 -4
View File
@@ -158,6 +158,14 @@ function formatMileage10(m: number): string {
return `${km}k${String(mm).padStart(3, '0')}`;
}
/** 미터값 → "158k163" (1m 단위) — 커서 배지용. 노선밖 표기(1m)와 갱신 밀도를 맞춘다. */
function formatMileage1(m: number): string {
const r = Math.round(m);
const km = Math.floor(r / 1000);
const mm = r % 1000;
return `${km}k${String(mm).padStart(3, '0')}`;
}
/**
* abcVideo 실제 영상 데이터 기반 측점 바.
* - 트랙 x축 = 프레임(시간) 진행. 구간 색 = 측점 km 증가(주황)/감소(하늘색).
@@ -457,7 +465,7 @@ export function StationBar({
realChain !== null && realChain >= 0
? (realOffM !== null && realOffM > OFF_ROUTE_M
? `노선밖 ${Math.round(realOffM)}m`
: formatMileage10(realChain))
: formatMileage1(realChain))
: '';
// viewedRef(프레임→측점 km) 추이로 구간 색·전환점 산출.
@@ -620,9 +628,12 @@ export function StationBar({
const out: { px: number; km: number }[] = [];
let i = 0;
while (i < arr.length) {
// 노선 밖(접근/이탈) 프레임 제외 — 폴리라인 밖에선 체이니지가 끝점(0 등)에 클램프되어
// 접근 구간 전체가 '시점(0k000) 통과'로 오인돼 마크가 바 맨 앞에 찍히는 문제 방지.
if (arr[i].offM > OFF_ROUTE_M) { i++; continue; }
if (Math.abs(arr[i].chain - targetM) < TH) {
let j = i, bj = i, bd = Math.abs(arr[i].chain - targetM);
while (j < arr.length && Math.abs(arr[j].chain - targetM) < TH) {
while (j < arr.length && arr[j].offM <= OFF_ROUTE_M && Math.abs(arr[j].chain - targetM) < TH) {
const d = Math.abs(arr[j].chain - targetM);
if (d < bd) { bd = d; bj = j; }
j++;
@@ -635,6 +646,19 @@ export function StationBar({
return out;
};
// 시점(첫 측점, 예: 0k000) 마크 — 드론이 노선 시작점을 통과하는 위치에 측점값 라벨 표시.
const appendStartMark = (marks: StructMark[]): StructMark[] => {
const valid = stations.filter(st => stationKm(st.title) >= 0);
if (!valid.length || !stationLine) return marks;
const first = valid.reduce((a, b) => (stationKm(a.title) <= stationKm(b.title) ? a : b));
const startM = stationKm(first.title);
for (const p of pxPassesAtMileage(startM)) {
// 측점값 라벨(위)이 "0k000" 을 이미 표시하므로 제목(아래)은 "시점" 만 — 중복 방지.
marks.push({ px: p.px, title: '시점', category: '측점', km: startM, kmExists: true });
}
return marks;
};
// 구조물 후보 POI(교량/터널/역사) base 이름 → 실좌표.
const cats = new Set(['교량', '터널', '역사']);
const poiByName = new Map<string, { lat: number; lon: number; category: string }>();
@@ -717,7 +741,7 @@ export function StationBar({
out.push({ px: p.px, title: s.name, category: cat, km: stationKmVal ?? p.km, kmExists, props: s.props });
}
}
return placeUnreachedTerminal(out);
return placeUnreachedTerminal(appendStartMark(out));
}
// 폴백: POI category 기반 (실좌표 GPS 근접, 모든 통과).
@@ -736,7 +760,7 @@ export function StationBar({
out.push({ px: p.px, title: base, category: info.category, km: stationKmVal ?? p.km, kmExists });
}
}
return placeUnreachedTerminal(out);
return placeUnreachedTerminal(appendStartMark(out));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ready, duration, storeFrames, pois, stations, pxAtTime, routeMeta, routeDirCh, storeStructures, pxAtMileage, endGapPx, stationLine, videoFps]);