- 클라이언트(React/Vite): Video.js 플레이어, 하단 스테이션바, POI/구조물 영상 오버레이, 카메라 파라미터 보정, 미니맵/RoutePanel - 서버(Express): Range 스트리밍, HLS 변환, 프레임 추출, tus 업로드, 주석 API - 최근 작업: 스테이션바 종점역 표출/양끝 정렬/미도착(재생불가) 표시, POI 라벨 '구분' 우선·컴팩트 팝업·겹침제외 토글·동일좌표 다중행, 진행방향(상/하) 우선 표출, 커서 배지 크기·픽셀 떨림 개선 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
/**
|
|
* 클라이언트 지리정보 스토어 (Zustand)
|
|
*
|
|
* 폴더 선택으로 파싱한 드론 프레임 / POI / 측점 / 중심선 / ENU 원점을 보관한다.
|
|
* 4개 소비 컴포넌트(GeoSearch / StationVerify / StationOverlay / RoutePanel)가
|
|
* 이 스토어를 단일 소스로 구독한다(서버 /api/geo/* 대체).
|
|
*
|
|
* playerStore.ts 패턴을 따른다.
|
|
*
|
|
* POI 위치 보정(poiOverrides): 마우스 드래그로 만든 title→{lat,lon,z} 맵.
|
|
* basePois(파싱 원본)에 applyPoiOverrides 를 적용한 결과가 pois 다.
|
|
* 내보내기/가져오기 파일(`<base>_poi_overrides.json`)로 영속화한다.
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import type {
|
|
DroneFrame,
|
|
GeoPoint,
|
|
CenterlinePoint,
|
|
GeoOrigin,
|
|
RouteMeta,
|
|
RouteStructure,
|
|
DirectionChange,
|
|
PoiOverride,
|
|
PoiOverrideMap,
|
|
} from '../types/geo';
|
|
import { loadFolderGeoData, applyPoiOverrides } from '../utils/geoData';
|
|
|
|
interface GeoStore {
|
|
loaded: boolean;
|
|
frames: DroneFrame[];
|
|
pois: GeoPoint[]; // type==='poi' (poiOverrides 적용 결과)
|
|
basePois: GeoPoint[]; // 파싱 원본 (보정 전) — 보정 재적용용
|
|
poiOverrides: PoiOverrideMap; // title → {lat,lon,z}
|
|
stations: GeoPoint[]; // type==='station' (stationOrder 정렬)
|
|
centerline: CenterlinePoint[];
|
|
origin: GeoOrigin | null;
|
|
baseName: string | null;
|
|
routeMeta: RouteMeta | null;
|
|
/** v2.0 CSV(03)교량/04)터널/06)구교) 유래 구조물 + route.json 보정. */
|
|
structures: RouteStructure[];
|
|
/** 측점 비고에서 추출한 방향전환점 목록. */
|
|
directionChanges: DirectionChange[];
|
|
|
|
/**
|
|
* 폴더 선택 파일에서 지리정보를 파싱해 스토어에 적재한다.
|
|
* 발견한 영상 File 을 반환(없으면 null) — 호출자가 loadLocalFile 로 재생.
|
|
*/
|
|
loadFromFolder: (files: FileList | File[]) => Promise<File | null>;
|
|
/** 단일 POI 보정 설정/갱신 (드래그 종료 시). title 키. */
|
|
setPoiOverride: (title: string, ov: PoiOverride) => void;
|
|
/** 단일 POI 보정 해제. */
|
|
clearPoiOverride: (title: string) => void;
|
|
/** 보정맵 전체 교체 (가져오기). */
|
|
setPoiOverrides: (map: PoiOverrideMap) => void;
|
|
clear: () => void;
|
|
}
|
|
|
|
const EMPTY = {
|
|
loaded: false,
|
|
frames: [] as DroneFrame[],
|
|
pois: [] as GeoPoint[],
|
|
basePois: [] as GeoPoint[],
|
|
poiOverrides: {} as PoiOverrideMap,
|
|
stations: [] as GeoPoint[],
|
|
centerline: [] as CenterlinePoint[],
|
|
origin: null as GeoOrigin | null,
|
|
baseName: null as string | null,
|
|
routeMeta: null as RouteMeta | null,
|
|
structures: [] as RouteStructure[],
|
|
directionChanges: [] as DirectionChange[],
|
|
};
|
|
|
|
export const useGeoStore = create<GeoStore>((set) => ({
|
|
...EMPTY,
|
|
|
|
loadFromFolder: async (files) => {
|
|
const data = await loadFolderGeoData(files);
|
|
set({
|
|
loaded: true,
|
|
frames: data.frames,
|
|
basePois: data.pois,
|
|
poiOverrides: data.poiOverrides,
|
|
pois: applyPoiOverrides(data.pois, data.poiOverrides),
|
|
stations: data.stations,
|
|
centerline: data.centerline,
|
|
origin: data.origin,
|
|
baseName: data.baseName,
|
|
routeMeta: data.routeMeta,
|
|
structures: data.structures,
|
|
directionChanges: data.directionChanges,
|
|
});
|
|
return data.videoFile;
|
|
},
|
|
|
|
setPoiOverride: (title, ov) =>
|
|
set((s) => {
|
|
const poiOverrides = { ...s.poiOverrides, [title]: ov };
|
|
return { poiOverrides, pois: applyPoiOverrides(s.basePois, poiOverrides) };
|
|
}),
|
|
|
|
clearPoiOverride: (title) =>
|
|
set((s) => {
|
|
const poiOverrides = { ...s.poiOverrides };
|
|
delete poiOverrides[title];
|
|
return { poiOverrides, pois: applyPoiOverrides(s.basePois, poiOverrides) };
|
|
}),
|
|
|
|
setPoiOverrides: (map) =>
|
|
set((s) => ({ poiOverrides: map, pois: applyPoiOverrides(s.basePois, map) })),
|
|
|
|
clear: () => set({ ...EMPTY }),
|
|
}));
|