- djmd(영상 내장 텔레메트리) 프레임 동기 위치/고도: MP4 stbl 선별 파싱(djmdTrack.ts), CSV 시각정렬 오차(평균 3.2m) 제거 — 세그먼트별 백그라운드 정밀화(frameSynced 플래그) - 자세 시간축 재정렬: djmd 위치 교차상관으로 CSV↔영상 오프셋 자동 추정(실측 -0.33s, 잔차 0.24m) 후 yaw/pitch/roll 재샘플(yaw 최단각) — 옆 지물 미끄러짐 해소 - 포즈 평활 자동 제한: 프레임 동기 데이터는 ±5프레임 상한(기존 ±60=±1s) — 회전 시 오버레이 전체가 드론에 딸려오는 지연 제거 - 선형-측점 정합: 측점 위치를 선형 정점으로 삽입(3D 이격≤0.012m) + 정점 z 를 측점 체이니지 보간으로 재정렬, 측점 라벨 화면 EMA 제거(선과 동일한 즉시 투영) — 선형이 측점 POI 를 픽셀 단위로 통과 - 드론높이 모드 높이 규칙 통일: 선형도 '드론고도-이격거리' 평면 적용(라벨과 동일) - 카메라 offZ 잔재 교정: 표고 체계 정비 이전 드래그 보정값(-18.3m)이 이중 보정이 되어 원거리 POI 가 접근할수록 밀리던 원인 — 경로 1-1 camera.json 0 으로 교정(.bak 백업) - 로드 UX: 폴더 로드 시 정지 상태로 대기(처음부터), 데이터 교체 시 UI 초기 상태 복원 - 라벨: 측점 팝업 표고(타원체고) 행 추가, 구조물-POI 이중 라벨 제거(어음천교), 시간동기 보정 슬라이더(timeOffsetSec) - 측점 표기 0+000 형식 전환(생성 6곳·파서 4곳 신구 호환) + 스테이션바 양끝 100m 내 시설물 없으면 시작/끝 스테이션 마크 - 단축키: F2 개발자 모드(dev_mode 1↔0, 세션 한정), A 선형 표시 토글 - 인프라: 54000 포트 재부팅 자동화(portproxy 직접 IP + 로그온 갱신, pm2 resurrect) - docs/history: 작업 이력 23건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
/** 측점값(체이니지) 공유 유틸 — 드론 GPS 를 측점 폴리라인에 투영해 측점값/최근접측점 산출.
|
|
* StationBar(배지)·VideoPlayer(측점 진단 HUD) 가 동일 계산을 공유한다. */
|
|
|
|
export interface ChainLine {
|
|
pts: { x: number; y: number; km: number; lat: number; lon: number; title: string }[];
|
|
k: number; // 경도→m 환산 (cos(lat0)*111000)
|
|
}
|
|
|
|
/** "157K970" → 157970(m). 실패 시 -1. */
|
|
export function kmFromTitle(title: string): number {
|
|
const m = title.match(/(\d+)[Kk](\d+)/);
|
|
return m ? parseInt(m[1], 10) * 1000 + parseInt(m[2], 10) : -1;
|
|
}
|
|
|
|
/** 미터값 → "157+970" (10m 단위). */
|
|
export function fmtKm10(m: number): string {
|
|
const r = Math.round(m / 10) * 10;
|
|
return `${Math.floor(r / 1000)}+${String(r % 1000).padStart(3, '0')}`;
|
|
}
|
|
|
|
/** 측점 POI 목록 → 평면투영 폴리라인(km 오름차순). 측점이 없으면 null. */
|
|
export function buildChainLine(stations: { title: string; lat: number; lon: number }[]): ChainLine | null {
|
|
const sts = stations.filter((s) => kmFromTitle(s.title) >= 0);
|
|
if (!sts.length) return null;
|
|
const sorted = [...sts].sort((a, b) => kmFromTitle(a.title) - kmFromTitle(b.title));
|
|
const lat0 = sorted.reduce((s, p) => s + p.lat, 0) / sorted.length;
|
|
const k = Math.cos((lat0 * Math.PI) / 180) * 111000;
|
|
return {
|
|
pts: sorted.map((p) => ({ x: p.lon * k, y: p.lat * 111000, km: kmFromTitle(p.title), lat: p.lat, lon: p.lon, title: p.title })),
|
|
k,
|
|
};
|
|
}
|
|
|
|
/** 드론 lat/lon 을 폴리라인에 투영 → { km(측점값), offsetM(선로 수직이격) }. */
|
|
export function projectToChain(lat: number, lon: number, line: ChainLine): { km: number; offsetM: number } {
|
|
const px = lon * line.k;
|
|
const py = lat * 111000;
|
|
const pts = line.pts;
|
|
let bestD = Infinity;
|
|
let bestKm = pts.length ? pts[0].km : -1;
|
|
for (let i = 0; i < pts.length - 1; i++) {
|
|
const a = pts[i], b = pts[i + 1];
|
|
const dx = b.x - a.x, dy = b.y - a.y, L2 = dx * dx + dy * dy;
|
|
const t = L2 === 0 ? 0 : Math.max(0, Math.min(1, ((px - a.x) * dx + (py - a.y) * dy) / L2));
|
|
const cx = a.x + dx * t, cy = a.y + dy * t;
|
|
const d = (px - cx) ** 2 + (py - cy) ** 2;
|
|
if (d < bestD) { bestD = d; bestKm = a.km + (b.km - a.km) * t; }
|
|
}
|
|
return { km: bestKm, offsetM: Math.sqrt(bestD) };
|
|
}
|
|
|
|
/** 드론 lat/lon 의 최근접 측점(점) → { title, km, distM }. */
|
|
export function nearestChainPoint(lat: number, lon: number, line: ChainLine): { title: string; km: number; distM: number } | null {
|
|
const pts = line.pts;
|
|
if (!pts.length) return null;
|
|
let nIdx = 0, nD = Infinity;
|
|
for (let i = 0; i < pts.length; i++) {
|
|
const dx = (lat - pts[i].lat) * 111000;
|
|
const dy = (lon - pts[i].lon) * line.k;
|
|
const d = dx * dx + dy * dy;
|
|
if (d < nD) { nD = d; nIdx = i; }
|
|
}
|
|
return { title: pts[nIdx].title, km: pts[nIdx].km, distM: Math.sqrt(nD) };
|
|
}
|