- terra-draw 기반 지도 폴리곤/사각형/원 그리기 + 편집 (OL Draw 대체) - 구역 항적 분석: ANY/ALL/SEQUENTIAL 검색모드, 다중구역 시각화 - STS 선박쌍 접촉 분석: 접촉쌍 그룹핑, 위험도 indicator, ScatterplotLayer - Deck.gl 레이어: PathLayer + TripsLayer + IconLayer (커서 기반 O(1) 보간) - 공유 타임라인 컨트롤 (재생/배속/프로그레스바) - CSV 내보내기 (다중 방문 동적 컬럼, BOM+UTF-8) - ApiExplorer 5모드 통합 (positions/vessel/replay/area-search/sts) 신규 17파일 (features/area-search/), 수정 5파일 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { create } from 'zustand'
|
|
import { PLAYBACK_SPEEDS } from '../types/areaSearch.types'
|
|
|
|
interface AnimationState {
|
|
isPlaying: boolean
|
|
currentTime: number // Unix epoch ms
|
|
startTime: number
|
|
endTime: number
|
|
playbackSpeed: number // 1x ~ 1000x
|
|
loop: boolean
|
|
animationFrameId: number | null
|
|
lastFrameTime: number
|
|
|
|
play: () => void
|
|
pause: () => void
|
|
stop: () => void
|
|
setPlaybackSpeed: (speed: number) => void
|
|
setCurrentTime: (time: number) => void
|
|
setProgressByRatio: (ratio: number) => void
|
|
setTimeRange: (start: number, end: number) => void
|
|
toggleLoop: () => void
|
|
getProgress: () => number // 0~100
|
|
reset: () => void
|
|
}
|
|
|
|
export { PLAYBACK_SPEEDS }
|
|
|
|
export const useAreaAnimationStore = create<AnimationState>((set, get) => ({
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
startTime: 0,
|
|
endTime: 0,
|
|
playbackSpeed: 100,
|
|
loop: false,
|
|
animationFrameId: null,
|
|
lastFrameTime: 0,
|
|
|
|
play: () => {
|
|
const state = get()
|
|
if (state.isPlaying) return
|
|
if (state.startTime >= state.endTime) return
|
|
|
|
if (state.currentTime >= state.endTime) {
|
|
set({ currentTime: state.startTime })
|
|
}
|
|
|
|
set({ isPlaying: true, lastFrameTime: performance.now() })
|
|
|
|
const animate = (timestamp: number) => {
|
|
const s = get()
|
|
if (!s.isPlaying) return
|
|
|
|
const deltaMs = timestamp - s.lastFrameTime
|
|
const increment = (deltaMs / 1000) * s.playbackSpeed * 1000
|
|
let newTime = s.currentTime + increment
|
|
|
|
if (newTime > s.endTime) {
|
|
if (s.loop) {
|
|
newTime = s.startTime
|
|
} else {
|
|
set({ isPlaying: false, currentTime: s.endTime, animationFrameId: null })
|
|
return
|
|
}
|
|
}
|
|
|
|
set({ currentTime: newTime, lastFrameTime: timestamp })
|
|
const frameId = requestAnimationFrame(animate)
|
|
set({ animationFrameId: frameId })
|
|
}
|
|
|
|
const frameId = requestAnimationFrame(animate)
|
|
set({ animationFrameId: frameId })
|
|
},
|
|
|
|
pause: () => {
|
|
const { animationFrameId } = get()
|
|
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId)
|
|
set({ isPlaying: false, animationFrameId: null })
|
|
},
|
|
|
|
stop: () => {
|
|
const { animationFrameId, startTime } = get()
|
|
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId)
|
|
set({ isPlaying: false, animationFrameId: null, currentTime: startTime })
|
|
},
|
|
|
|
setPlaybackSpeed: (speed) => set({ playbackSpeed: speed }),
|
|
|
|
setCurrentTime: (time) => set({ currentTime: time }),
|
|
|
|
setProgressByRatio: (ratio) => {
|
|
const { startTime, endTime } = get()
|
|
const time = startTime + (endTime - startTime) * Math.max(0, Math.min(1, ratio))
|
|
set({ currentTime: time })
|
|
},
|
|
|
|
setTimeRange: (start, end) => set({ startTime: start, endTime: end, currentTime: start }),
|
|
|
|
toggleLoop: () => set((s) => ({ loop: !s.loop })),
|
|
|
|
getProgress: () => {
|
|
const { currentTime, startTime, endTime } = get()
|
|
if (endTime <= startTime) return 0
|
|
return ((currentTime - startTime) / (endTime - startTime)) * 100
|
|
},
|
|
|
|
reset: () => {
|
|
const { animationFrameId } = get()
|
|
if (animationFrameId !== null) cancelAnimationFrame(animationFrameId)
|
|
set({
|
|
isPlaying: false,
|
|
currentTime: 0,
|
|
startTime: 0,
|
|
endTime: 0,
|
|
playbackSpeed: 100,
|
|
loop: false,
|
|
animationFrameId: null,
|
|
lastFrameTime: 0,
|
|
})
|
|
},
|
|
}))
|