feat: StationOverlay 렌더링 최적화 및 스무딩 적용 close #1
- 텍스트(측점/POI) 전 프레임 사전 계산 Map (requestIdleCallback 백그라운드) - 드론 데이터 이동 평균 스무딩 (smoothFrame ±N프레임) - 30fps→60fps 프레임 간 선형 보간 (performance.now() 기반) - EMA(지수이동평균) 표시 위치 스무딩 (α=0.01 기본값) - 글씨 2배 크기, bold, strokeText 테두리, 배경 박스 제거 - 카메라 파라미터 패널에 smooth/EMA α 슬라이더 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
94
server/src/routes/geo.ts
Normal file
94
server/src/routes/geo.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Router } from 'express';
|
||||
import path from 'path';
|
||||
import { setGeoDataDir, findFramesForPoi, findPoisForFrame, getAllPois, getDroneFrames, getCenterlinePoints } from '../services/geoMatch';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// 드론 CSV + building 폴더 위치 설정
|
||||
const GEO_DATA_DIR = process.env.GEO_DATA_DIR ||
|
||||
path.resolve(__dirname, '../../../samplevideo');
|
||||
|
||||
setGeoDataDir(GEO_DATA_DIR);
|
||||
|
||||
/** POI/측점 전체 목록 (자동완성용) */
|
||||
router.get('/pois', (_req, res) => {
|
||||
try {
|
||||
const pois = getAllPois();
|
||||
res.json(pois);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 건물/측점명 검색 → 해당 POI가 카메라에 보이는 프레임 목록
|
||||
* GET /api/geo/search?q=회덕역&margin=1.0
|
||||
*/
|
||||
router.get('/search', (req, res) => {
|
||||
const q = String(req.query.q || '').trim();
|
||||
const margin = parseFloat(String(req.query.margin || '1.0'));
|
||||
const yawOffset = parseFloat(String(req.query.yawOffset || '0'));
|
||||
if (!q) return res.status(400).json({ error: 'q 파라미터 필요' });
|
||||
|
||||
try {
|
||||
const result = findFramesForPoi(
|
||||
q,
|
||||
isNaN(margin) ? 1.0 : margin,
|
||||
parseFloat(String(req.query.maxDist || '2000')),
|
||||
isNaN(yawOffset) ? 0 : yawOffset,
|
||||
);
|
||||
if (!result.poi) return res.status(404).json({ error: `"${q}" POI를 찾을 수 없습니다` });
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 특정 프레임에서 보이는 POI/측점 목록
|
||||
* GET /api/geo/frame/1234?margin=1.0
|
||||
*/
|
||||
router.get('/frame/:frameNum', (req, res) => {
|
||||
const frameNum = parseInt(req.params.frameNum, 10);
|
||||
const margin = parseFloat(String(req.query.margin || '1.0'));
|
||||
const yawOffset = parseFloat(String(req.query.yawOffset || '0'));
|
||||
if (isNaN(frameNum)) return res.status(400).json({ error: '유효한 프레임 번호 필요' });
|
||||
|
||||
try {
|
||||
const result = findPoisForFrame(frameNum, isNaN(margin) ? 1.0 : margin, isNaN(yawOffset) ? 0 : yawOffset);
|
||||
if (!result.droneFrame) return res.status(404).json({ error: `프레임 ${frameNum}을 찾을 수 없습니다` });
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 드론 비행경로 전체 데이터 (경로 시각화용)
|
||||
* GET /api/geo/frames?step=30 step: 샘플링 간격 (기본 30 = 1초마다)
|
||||
*/
|
||||
router.get('/frames', (req, res) => {
|
||||
const step = Math.max(1, parseInt(String(req.query.step || '30'), 10));
|
||||
try {
|
||||
const all = getDroneFrames();
|
||||
const sampled = all.filter((_, i) => i % step === 0);
|
||||
res.json(sampled);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 선로 중심선 전체 좌표 (center.csv, 224점)
|
||||
* GET /api/geo/centerline
|
||||
*/
|
||||
router.get('/centerline', (_req, res) => {
|
||||
try {
|
||||
const pts = getCenterlinePoints();
|
||||
res.json(pts);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: String(e) });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user