- 타원체고 기준 자동 변환: route.json ellipsoidal_height 선언 시 드론 고도(CSV 정표고)를 영상 메타(djmd 6.6 타원체고)와의 차(지오이드고 자동 산출)만큼 보정, 세그먼트 전환에도 유지 - KML 표고 파이프라인: 좌표 고도(z) 사용(POI/측점), *_byDem.kml 자동 생성(DEM 조회·높이 속성 포함) + 로더의 _byDem 우선 규칙, 선형중심선(LineString) 로딩·경로표고 앵커 - 설정 파일: camera.json/display.json/route.json 단순 이름 인식, 화면표시 옵션 PC 저장/자동 적용, 초기화 버튼을 환경설정 파일 기준 복원으로 변경, 저장 파일명 단순화 - 선형 표시: 표출된 마지막 측점까지 클립(경계 세그먼트 절단 + 관통 현 처리 — 구간 실종 수정) - 스테이션바: 시점(0k000) 마크(실통과 위치), 노선밖 프레임 통과 오인 방지, 커서 배지 1m 단위, 배너 연장/소요 실측값(측점 체이니지·영상 길이 합) 우선 - 세그먼트 전환 라벨 깜빡임 구조 수정(캐시 초기화·최근접 키 조회·이중 재계산 제거) + 사전계산 프레임 불변값 호이스팅(재생 지연 해소) - UI: 슬라이더/값 입력칸 휠 조절, 패널 버튼 위치 정렬, 저장 버튼 중복 제거, dev_mode(route.json)로 영상제어 UI 표시 제어, 지오이드 최대 200, 이격거리 측점 동반 적용 - 시연: start-demo.bat + docs/DEMO.md - docs/history: 작업 이력 30건 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
5.2 KiB
TypeScript
116 lines
5.2 KiB
TypeScript
/**
|
|
* KML 표고(DEM) 파생 파일 생성 — `<이름>_byDem.kml`
|
|
*
|
|
* 폴더 로드 시 측점/지장물 KML 의 좌표 고도 성분이 없거나 0 이면, DEM(서버 /api/elevation
|
|
* 프록시 → SRTM 30m, 폴백 90m)으로 지면 표고를 조회해 채운 `<이름>_byDem.kml` 을 만들어
|
|
* PC 로 다운로드한다. 사용자가 받은 파일을 영상 폴더에 넣으면, 로더(parseKmz)가
|
|
* 원본 대신 _byDem 버전을 읽는다(중복 파싱 방지 규칙).
|
|
*
|
|
* - 이미 표고가 있는 좌표(>0)는 그대로 두고 없는 것만 채운다
|
|
* - 폴더에 `<이름>_byDem.kml` 이 이미 있으면 해당 원본은 건너뜀
|
|
* - DEM 조회는 100좌표/청크, 업스트림 rate limit(1req/s) 고려 1.2s 간격
|
|
*/
|
|
|
|
/** File 의 폴더 내 파일명 (webkitRelativePath 마지막 세그먼트). */
|
|
function baseNameOf(f: File): string {
|
|
const p = f.webkitRelativePath || f.name;
|
|
return p.split('/').pop() ?? f.name;
|
|
}
|
|
|
|
function isInBuilding(f: File): boolean {
|
|
const parts = (f.webkitRelativePath || f.name).split('/');
|
|
return parts.length >= 2 && parts[parts.length - 2].toLowerCase() === 'building';
|
|
}
|
|
|
|
/** 표고가 필요한(없거나 0) 좌표 목록 수집. */
|
|
function collectMissing(text: string): { key: string; lat: number; lon: number }[] {
|
|
const need: { key: string; lat: number; lon: number }[] = [];
|
|
const seen = new Set<string>();
|
|
for (const block of text.matchAll(/<coordinates>([\s\S]*?)<\/coordinates>/g)) {
|
|
for (const tok of block[1].trim().split(/\s+/)) {
|
|
const p = tok.split(',');
|
|
if (p.length < 2) continue;
|
|
const lon = parseFloat(p[0]), lat = parseFloat(p[1]);
|
|
if (!isFinite(lon) || !isFinite(lat)) continue;
|
|
const alt = p.length >= 3 ? parseFloat(p[2]) : NaN;
|
|
if (isFinite(alt) && alt > 0) continue; // 이미 표고 보유
|
|
const key = `${p[0]},${p[1]}`;
|
|
if (!seen.has(key)) { seen.add(key); need.push({ key, lat, lon }); }
|
|
}
|
|
}
|
|
return need;
|
|
}
|
|
|
|
/**
|
|
* 폴더 파일들에서 표고 없는 KML 을 찾아 `<이름>_byDem.kml` 을 생성·다운로드한다.
|
|
* 생성한 파일명 목록을 반환(생성할 것이 없으면 빈 배열).
|
|
*/
|
|
export async function generateByDemKmls(input: FileList | File[]): Promise<string[]> {
|
|
const files = Array.from(input);
|
|
const kmls = files.filter((f) => !isInBuilding(f) && /\.kml$/i.test(baseNameOf(f)));
|
|
const lowerNames = new Set(kmls.map((f) => baseNameOf(f).toLowerCase()));
|
|
const made: string[] = [];
|
|
|
|
for (const f of kmls) {
|
|
const base = baseNameOf(f).replace(/\.kml$/i, '');
|
|
if (/_bydem$/i.test(base)) continue; // 자신이 DEM 버전
|
|
if (lowerNames.has(`${base.toLowerCase()}_bydem.kml`)) continue; // DEM 버전이 이미 폴더에 있음
|
|
|
|
const text = await f.text();
|
|
const need = collectMissing(text);
|
|
if (!need.length) continue; // 전 좌표 표고 보유 → 불필요
|
|
|
|
// DEM 조회 (서버 프록시)
|
|
const elev = new Map<string, number>();
|
|
for (let i = 0; i < need.length; i += 100) {
|
|
const chunk = need.slice(i, i + 100);
|
|
const r = await fetch(
|
|
`/api/elevation?lat=${chunk.map((c) => c.lat).join(',')}&lon=${chunk.map((c) => c.lon).join(',')}`,
|
|
);
|
|
if (!r.ok) throw new Error(`DEM 조회 실패 (HTTP ${r.status})`);
|
|
const j: { elevation?: (number | null)[] } = await r.json();
|
|
chunk.forEach((c, k) => {
|
|
const e = j.elevation?.[k];
|
|
if (typeof e === 'number' && isFinite(e)) elev.set(c.key, e);
|
|
});
|
|
if (i + 100 < need.length) await new Promise((res) => setTimeout(res, 1200));
|
|
}
|
|
if (!elev.size) continue;
|
|
|
|
// 고도 성분 치환(없거나 0 인 것만) — 그 외 텍스트/구조는 원본 그대로 보존
|
|
let next = text.replace(/<coordinates>([\s\S]*?)<\/coordinates>/g, (_m, body: string) =>
|
|
`<coordinates>${body.replace(/(\S+)/g, (tok: string) => {
|
|
const p = tok.split(',');
|
|
if (p.length < 2) return tok;
|
|
const alt = p.length >= 3 ? parseFloat(p[2]) : NaN;
|
|
if (isFinite(alt) && alt > 0) return tok;
|
|
const e = elev.get(`${p[0]},${p[1]}`);
|
|
return e === undefined ? tok : `${p[0]},${p[1]},${e.toFixed(1)}`;
|
|
})}</coordinates>`);
|
|
|
|
// '높이' 속성(지장물 스키마)도 0/빈값이면 같은 DEM 표고로 채운다 — Placemark 단위로
|
|
// 자기 좌표의 표고를 대응. 실제 값이 있는 높이는 보존.
|
|
next = next.replace(/<Placemark[^>]*>[\s\S]*?<\/Placemark>/g, (pm) => {
|
|
const cm = pm.match(/<coordinates>\s*([-\d.]+),([-\d.]+)/);
|
|
if (!cm) return pm;
|
|
const e = elev.get(`${cm[1]},${cm[2]}`);
|
|
if (e === undefined) return pm;
|
|
return pm.replace(
|
|
/(<SimpleData name="높이">)\s*(?:0(?:\.0+)?)?\s*(<\/SimpleData>)/,
|
|
`$1${e.toFixed(1)}$2`,
|
|
);
|
|
});
|
|
|
|
const outName = `${base}_byDem.kml`;
|
|
const url = URL.createObjectURL(new Blob([next], { type: 'application/vnd.google-earth.kml+xml' }));
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = outName;
|
|
a.click();
|
|
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
made.push(outName);
|
|
console.log(`[DEM] ${outName} 생성 — 좌표 ${need.length}개 중 ${elev.size}개 표고 기록`);
|
|
}
|
|
return made;
|
|
}
|