ship-gis/src/replay/stores/mergedTrackStore.js
HeungTak Lee b209c9498c feat: 리플레이 모드 구현
- replay 패키지 (stores, components, hooks, services, utils, types)
- WebSocket 기반 청크 데이터 수신 (ReplayWebSocketService)
- 시간 기반 애니메이션 (재생/일시정지/정지, 배속 1x~1000x)
- 항적 표시 토글 (playbackTrailStore - 프레임 기반 페이딩)
- 선박 상태 관리 + 필터링 (선종, 신호원)
- 드래그 가능한 타임라인 컨트롤러
- 라이브/리플레이 전환 (liveControl)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 06:37:20 +09:00

201 lines
4.7 KiB
JavaScript

/**
* 리플레이 데이터 저장소
* 참조: src/tracking/stores/mergedTrackStore.ts
*
* 청크 기반 선박 항적 데이터 저장 및 관리
*/
import { create } from 'zustand';
import { parseTimestamp } from '../types/replay.types';
/**
* 청크 기반 선박 데이터 병합
*/
function mergeVesselChunks(existingChunks, newChunk) {
if (!existingChunks) {
return {
vesselId: newChunk.vesselId,
sigSrcCd: newChunk.sigSrcCd,
targetId: newChunk.targetId,
shipName: newChunk.shipName,
shipKindCode: newChunk.shipKindCode,
chunks: [newChunk],
cachedPath: null,
totalDistance: newChunk.totalDistance || 0,
maxSpeed: newChunk.maxSpeed || 0,
avgSpeed: newChunk.avgSpeed || 0,
};
}
// 기존 청크에 새 청크 추가 (시간순 정렬)
const chunks = [...existingChunks.chunks, newChunk].sort((a, b) => {
const timeA = parseTimestamp(a.timestamps[0]);
const timeB = parseTimestamp(b.timestamps[0]);
return timeA - timeB;
});
return {
...existingChunks,
chunks,
cachedPath: null, // 캐시 무효화
totalDistance: Math.max(existingChunks.totalDistance, newChunk.totalDistance || 0),
maxSpeed: Math.max(existingChunks.maxSpeed, newChunk.maxSpeed || 0),
};
}
/**
* 병합된 경로 생성 (캐싱)
*/
function buildCachedPath(chunks) {
const geometry = [];
const timestamps = [];
const timestampsMs = [];
const speeds = [];
chunks.forEach((chunk) => {
if (chunk.geometry) {
geometry.push(...chunk.geometry);
}
if (chunk.timestamps) {
timestamps.push(...chunk.timestamps);
chunk.timestamps.forEach((ts) => {
timestampsMs.push(parseTimestamp(ts));
});
}
if (chunk.speeds) {
speeds.push(...chunk.speeds);
}
});
return {
geometry,
timestamps,
timestampsMs,
speeds,
lastUpdated: Date.now(),
};
}
/**
* MergedTrackStore
*/
const useMergedTrackStore = create((set, get) => ({
// ===== 상태 =====
// 청크 기반 저장소 (메인)
vesselChunks: new Map(), // Map<vesselId, VesselChunks>
// 원본 청크 (리플레이용)
rawChunks: [],
// 메타데이터
timeRange: null, // { start: number, end: number }
spatialBounds: null, // { minLon, maxLon, minLat, maxLat }
// ===== 액션 =====
/**
* 청크 추가 (최적화)
*/
addChunkOptimized: (chunkResponse) => {
const tracks = chunkResponse.tracks || [];
set((state) => {
const newVesselChunks = new Map(state.vesselChunks);
let timeRange = state.timeRange;
tracks.forEach((track) => {
const vesselId = track.vesselId;
const existingChunks = newVesselChunks.get(vesselId);
const mergedChunks = mergeVesselChunks(existingChunks, track);
newVesselChunks.set(vesselId, mergedChunks);
// 시간 범위 업데이트
if (track.timestamps && track.timestamps.length > 0) {
const firstTime = parseTimestamp(track.timestamps[0]);
const lastTime = parseTimestamp(track.timestamps[track.timestamps.length - 1]);
if (!timeRange) {
timeRange = { start: firstTime, end: lastTime };
} else {
timeRange = {
start: Math.min(timeRange.start, firstTime),
end: Math.max(timeRange.end, lastTime),
};
}
}
});
return {
vesselChunks: newVesselChunks,
timeRange,
};
});
},
/**
* 병합된 경로 반환 (캐시 사용)
*/
getMergedPath: (vesselId) => {
const vesselChunks = get().vesselChunks.get(vesselId);
if (!vesselChunks) return null;
// 캐시 확인
if (vesselChunks.cachedPath) {
return vesselChunks.cachedPath;
}
// 새로 생성
const cachedPath = buildCachedPath(vesselChunks.chunks);
// 캐시 저장
set((state) => {
const newVesselChunks = new Map(state.vesselChunks);
newVesselChunks.set(vesselId, {
...vesselChunks,
cachedPath,
});
return { vesselChunks: newVesselChunks };
});
return cachedPath;
},
/**
* 원본 청크 추가
*/
addRawChunk: (chunkResponse) => {
set((state) => ({
rawChunks: [...state.rawChunks, chunkResponse],
}));
},
/**
* 전체 초기화
*/
clear: () => {
set({
vesselChunks: new Map(),
rawChunks: [],
timeRange: null,
spatialBounds: null,
});
},
/**
* 모든 선박 ID 반환
*/
getAllVesselIds: () => {
return Array.from(get().vesselChunks.keys());
},
/**
* 선박 데이터 반환
*/
getVesselChunks: (vesselId) => {
return get().vesselChunks.get(vesselId) || null;
},
}));
export default useMergedTrackStore;