- 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>
63 lines
2.1 KiB
TypeScript
Executable File
63 lines
2.1 KiB
TypeScript
Executable File
import { ROUTE_LEGS } from '../mocks/route';
|
|
import type { RouteLeg } from '../types/timeline';
|
|
|
|
const MILEAGE_ROUND_M = 10;
|
|
const METERS_PER_KM = 1000;
|
|
|
|
/** Mileage in meters at a cursor px, via piecewise-linear leg anchors. */
|
|
export function mileageAtPx(px: number, legs: RouteLeg[] = ROUTE_LEGS): number {
|
|
const leg =
|
|
legs.find((l) => px >= l.startPx && px <= l.endPx) ?? legs[legs.length - 1];
|
|
const anchors = leg.anchors;
|
|
for (let i = 0; i < anchors.length - 1; i++) {
|
|
const a = anchors[i];
|
|
const b = anchors[i + 1];
|
|
if (px <= b.px || i === anchors.length - 2) {
|
|
const t = Math.min(1, Math.max(0, (px - a.px) / (b.px - a.px)));
|
|
return a.mileage + (b.mileage - a.mileage) * t;
|
|
}
|
|
}
|
|
return anchors[0].mileage;
|
|
}
|
|
|
|
/**
|
|
* First px at which the given mileage occurs. Leg start anchors win so a jump
|
|
* to a turn mileage lands on the start of that leg.
|
|
*/
|
|
export function pxForMileage(
|
|
mileage: number,
|
|
legs: RouteLeg[] = ROUTE_LEGS,
|
|
): number | null {
|
|
for (const leg of legs) {
|
|
if (leg.anchors[0].mileage === mileage) return leg.startPx;
|
|
}
|
|
for (const leg of legs) {
|
|
const anchors = leg.anchors;
|
|
for (let i = 0; i < anchors.length - 1; i++) {
|
|
const a = anchors[i];
|
|
const b = anchors[i + 1];
|
|
const lo = Math.min(a.mileage, b.mileage);
|
|
const hi = Math.max(a.mileage, b.mileage);
|
|
if (mileage >= lo && mileage <= hi) {
|
|
return a.px + (b.px - a.px) * ((mileage - a.mileage) / (b.mileage - a.mileage));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** 158204 → "158+200". */
|
|
export function formatMileage(mileage: number): string {
|
|
const rounded = Math.round(mileage / MILEAGE_ROUND_M) * MILEAGE_ROUND_M;
|
|
const km = Math.floor(rounded / METERS_PER_KM);
|
|
const m = rounded % METERS_PER_KM;
|
|
return `${km}+${String(m).padStart(3, '0')}`;
|
|
}
|
|
|
|
/** Accepts "158k200", "158200", "161800"… Returns meters or null. */
|
|
export function parseMileageQuery(raw: string): number | null {
|
|
const digits = raw.toLowerCase().replace(/k/g, '').replace(/[^0-9]/g, '');
|
|
const mileage = parseInt(digits, 10);
|
|
return Number.isFinite(mileage) && mileage > 0 ? mileage : null;
|
|
}
|