- 클라이언트(React/Vite): Video.js 플레이어, 하단 스테이션바, POI/구조물 영상 오버레이, 카메라 파라미터 보정, 미니맵/RoutePanel - 서버(Express): Range 스트리밍, HLS 변환, 프레임 추출, tus 업로드, 주석 API - 최근 작업: 스테이션바 종점역 표출/양끝 정렬/미도착(재생불가) 표시, POI 라벨 '구분' 우선·컴팩트 팝업·겹침제외 토글·동일좌표 다중행, 진행방향(상/하) 우선 표출, 커서 배지 크기·픽셀 떨림 개선 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
import { Router } from 'express';
|
|
|
|
const router = Router();
|
|
|
|
/**
|
|
* DEM 표고 프록시 — 브라우저 COEP(require-corp)/CSP(connect-src 'self') 때문에
|
|
* 클라이언트가 외부 고도 API를 직접 호출하지 못한다. 서버가 중계해 같은 출처로 응답.
|
|
*
|
|
* GET /api/elevation?lat=36.4,36.5&lon=127.4,127.5 (배치, 콤마 구분)
|
|
* 응답: { elevation: number[] } (입력 순서 보존)
|
|
*
|
|
* 1순위 open-topodata SRTM 30m(절벽/지형 더 정밀), 실패 시 open-meteo 90m 폴백.
|
|
*/
|
|
router.get('/', async (req, res) => {
|
|
const latStr = String(req.query.lat ?? req.query.latitude ?? '');
|
|
const lonStr = String(req.query.lon ?? req.query.longitude ?? '');
|
|
if (!latStr || !lonStr) {
|
|
res.status(400).json({ error: 'lat/lon required' });
|
|
return;
|
|
}
|
|
const lats = latStr.split(',');
|
|
const lons = lonStr.split(',');
|
|
if (lats.length !== lons.length) {
|
|
res.status(400).json({ error: 'lat/lon length mismatch' });
|
|
return;
|
|
}
|
|
|
|
// 1) open-topodata SRTM 30m
|
|
try {
|
|
const locs = lats.map((la, i) => `${la.trim()},${lons[i].trim()}`).join('|');
|
|
const r = await fetch(
|
|
'https://api.opentopodata.org/v1/srtm30m?locations=' + encodeURIComponent(locs),
|
|
);
|
|
if (r.ok) {
|
|
const j: any = await r.json();
|
|
if (Array.isArray(j?.results)) {
|
|
const elevation = j.results.map((x: any) =>
|
|
typeof x?.elevation === 'number' ? x.elevation : null,
|
|
);
|
|
if (elevation.some((v: number | null) => v != null)) {
|
|
res.json({ elevation, source: 'opentopodata-srtm30m' });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
/* 폴백으로 진행 */
|
|
}
|
|
|
|
// 2) open-meteo 90m 폴백
|
|
try {
|
|
const r = await fetch(
|
|
'https://api.open-meteo.com/v1/elevation?latitude=' +
|
|
encodeURIComponent(latStr) +
|
|
'&longitude=' +
|
|
encodeURIComponent(lonStr),
|
|
);
|
|
if (!r.ok) {
|
|
res.status(502).json({ error: 'upstream ' + r.status });
|
|
return;
|
|
}
|
|
const j: any = await r.json();
|
|
res.json({ elevation: j?.elevation ?? [], source: 'open-meteo-90m' });
|
|
} catch (e) {
|
|
res.status(502).json({ error: String(e) });
|
|
}
|
|
});
|
|
|
|
export default router;
|