/** * 측점 검증 패널 * - 측점 목록을 클릭하면 해당 측점이 가장 잘 보이는 프레임으로 이동 * - 이동 결과(거리, 화면 위치)를 표시하여 계산 정확도 검증 * * 데이터 소스: 클라이언트 geoStore(폴더 선택으로 파싱). 폴더 미선택 시 빈 상태. */ import React, { useState } from 'react'; import { useGeoStore } from '../../store/geoStore'; import { findFramesForPoi } from '../../utils/geoSearch'; import type { GeoPoint, FrameMatch } from '../../types/geo'; interface StationResult { frames: FrameMatch[]; poi: GeoPoint; } interface Props { fps: number; onSeekToFrame: (frame: number) => void; } export default function StationVerify({ fps, onSeekToFrame }: Props) { // stations 는 이미 stationOrder 로 정렬되어 있다. const stations = useGeoStore(s => s.stations); const [selected, setSelected] = useState(null); const [result, setResult] = useState(null); const [seekedFrame, setSeekedFrame] = useState(null); // 드론 GPS 가 측점에 가장 가까운 프레임 (StationBar 배지·실제 위치와 동일 기준). const nearestFrameForStation = (st: GeoPoint): number | null => { const fr = useGeoStore.getState().frames; if (!fr.length) return null; let best = fr[0]; let bd = (fr[0].lat - st.lat) ** 2 + (fr[0].lon - st.lon) ** 2; for (const f of fr) { const d = (f.lat - st.lat) ** 2 + (f.lon - st.lon) ** 2; if (d < bd) { bd = d; best = f; } } return best.frame; }; const handleClick = (station: GeoPoint) => { setSelected(station.title); setResult(null); setSeekedFrame(null); // 영상은 드론 GPS 가 그 측점에 가장 가까운 프레임으로 이동한다. // (카메라 FOV 검색은 앞을 보는 카메라 특성상 ~200m 앞쪽으로 치우쳐 위치가 어긋남) const gpsFrame = nearestFrameForStation(station); if (gpsFrame != null) { onSeekToFrame(gpsFrame); setSeekedFrame(gpsFrame); } // 검증 정보(카메라 시야 프레임/투영)는 참고용으로 표시. const { frames, pois, stations: sts, origin } = useGeoStore.getState(); const { poi, frames: matches } = findFramesForPoi( frames, [...sts, ...pois], station.title, 1.2, 2000, 0, origin, ); setResult({ frames: matches, poi: poi ?? station }); }; const pixelQuality = (px: number, py: number) => { const dx = Math.abs(px - 0.5); const dy = Math.abs(py - 0.5); const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 0.15) return 'text-green-400'; if (dist < 0.35) return 'text-yellow-400'; return 'text-orange-400'; }; return (
측점 클릭 → 최적 프레임 이동 + 검증
{stations.length}개 측점
{/* 선택된 측점 결과 */} {selected && (
{selected}
{result && result.frames.length === 0 && (
카메라 시야에 들어오는 프레임 없음
)} {result && result.frames.length > 0 && (() => { const f = result.frames[0]; return ( <>
F{f.frame} · {f.distance >= 1000 ? `${(f.distance/1000).toFixed(2)}km` : `${Math.round(f.distance)}m`}
화면 ({(f.pixelX * 100).toFixed(0)}%, {(f.pixelY * 100).toFixed(0)}%)  수평 {f.bearingDiff >= 0 ? '+' : ''}{f.bearingDiff.toFixed(1)}°
{result.frames.length > 1 && (
{result.frames.slice(1).map((fm, i) => ( ))}
)} ); })()}
)} {/* 측점 목록 */}
{stations.map(st => ( ))}
); }