/** * 측점 검증 패널 * - 측점 목록을 클릭하면 해당 측점이 가장 잘 보이는 프레임으로 이동 * - 이동 결과(거리, 화면 위치)를 표시하여 계산 정확도 검증 */ import React, { useState, useEffect } from 'react'; interface GeoPoint { title: string; category: string; lat: number; lon: number; z: number; type: 'poi' | 'station'; } interface FrameMatch { frame: number; time: number; bearingDiff: number; elevationDiff: number; distance: number; pixelX: number; pixelY: number; groupSize?: number; groupStart?: number; groupEnd?: number; } interface StationResult { frames: FrameMatch[]; poi: GeoPoint; } interface Props { fps: number; onSeekToFrame: (frame: number) => void; } function stationOrder(title: string): number { const m = title.match(/(\d+)[Kk](\d+)/); if (!m) return 0; return parseInt(m[1]) * 1000 + parseInt(m[2]); } export default function StationVerify({ fps, onSeekToFrame }: Props) { const [stations, setStations] = useState([]); const [selected, setSelected] = useState(null); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [seekedFrame, setSeekedFrame] = useState(null); useEffect(() => { fetch('/api/geo/pois') .then(r => r.json()) .then((data: GeoPoint[]) => { const s = Array.isArray(data) ? data.filter(p => p.type === 'station').sort((a, b) => stationOrder(a.title) - stationOrder(b.title)) : []; setStations(s); }) .catch(() => {}); }, []); const handleClick = async (station: GeoPoint) => { setSelected(station.title); setResult(null); setLoading(true); setSeekedFrame(null); try { const res = await fetch(`/api/geo/search?q=${encodeURIComponent(station.title)}&margin=1.2&maxDist=2000`); const data = await res.json(); if (!res.ok || !data.frames?.length) { setResult({ frames: [], poi: data.poi ?? station }); return; } setResult(data); // 가장 중심에 가까운 첫 번째 결과로 이동 const best = data.frames[0]; onSeekToFrame(best.frame); setSeekedFrame(best.frame); } catch { setResult(null); } finally { setLoading(false); } }; 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}
{loading &&
검색 중…
} {!loading && result && result.frames.length === 0 && (
카메라 시야에 들어오는 프레임 없음
)} {!loading && 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 => ( ))}
); }