245 lines
7.6 KiB
TypeScript
245 lines
7.6 KiB
TypeScript
import { useState, useCallback, useRef, useEffect } from 'react'
|
||
import { useImageFrameLoader } from './use-image-frame-loader'
|
||
|
||
interface UseRadarTileOptions {
|
||
maxBufferSize?: number;
|
||
targetPrefetch?: number;
|
||
preCacheCount?: number; // 前后各预缓存N个帧
|
||
generateUrl?: (timestamp: number) => string; // 根据时间戳生成URL的函数
|
||
}
|
||
|
||
interface RadarTileFrame {
|
||
timestamp: number;
|
||
frameIndex: number;
|
||
url: string;
|
||
}
|
||
|
||
export function useRadarTile({
|
||
maxBufferSize = 12,
|
||
targetPrefetch = 4,
|
||
preCacheCount = 3,
|
||
generateUrl
|
||
}: UseRadarTileOptions = {}) {
|
||
|
||
const [currentFrame, setCurrentFrame] = useState<RadarTileFrame | null>(null)
|
||
const [currentBitmap, setCurrentBitmap] = useState<ImageBitmap | null>(null)
|
||
const [isLoading, setIsLoading] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
|
||
// 时间戳到帧索引的映射缓存
|
||
const timestampToFrameMap = useRef<Map<number, number>>(new Map())
|
||
// 订阅取消函数
|
||
const unsubscribeRef = useRef<(() => void) | null>(null)
|
||
|
||
const {
|
||
loadFrame,
|
||
clearCache,
|
||
getFrame,
|
||
isFrameCached,
|
||
getCacheStats,
|
||
subscribeToFrame,
|
||
isLoading: frameLoaderIsLoading,
|
||
error: frameLoaderError,
|
||
isWorkerSupported
|
||
} = useImageFrameLoader({
|
||
maxBufferSize,
|
||
targetPrefetch
|
||
})
|
||
|
||
// 生成稳定的帧索引(基于时间戳)
|
||
const getFrameIndex = useCallback((timestamp: number): number => {
|
||
if (timestampToFrameMap.current.has(timestamp)) {
|
||
return timestampToFrameMap.current.get(timestamp)!
|
||
}
|
||
|
||
const frameIndex = Math.abs(timestamp.toString().split('').reduce((a, b) => {
|
||
a = ((a << 5) - a) + parseInt(b) || 0
|
||
return a & a
|
||
}, 0))
|
||
|
||
timestampToFrameMap.current.set(timestamp, frameIndex)
|
||
return frameIndex
|
||
}, [])
|
||
|
||
// 基础功能:切换到指定时间的帧
|
||
const switchToTime = useCallback(async (timestamp: number) => {
|
||
if (!generateUrl) {
|
||
setError('generateUrl function is required')
|
||
return null
|
||
}
|
||
|
||
const url = generateUrl(timestamp)
|
||
const frameIndex = getFrameIndex(timestamp)
|
||
|
||
|
||
const frame: RadarTileFrame = {
|
||
timestamp,
|
||
frameIndex,
|
||
url
|
||
}
|
||
|
||
setCurrentFrame(frame)
|
||
setError(null)
|
||
setIsLoading(true)
|
||
|
||
// 检查是否已缓存
|
||
const cachedBitmap = getFrame(frameIndex)
|
||
if (cachedBitmap) {
|
||
setCurrentBitmap(cachedBitmap)
|
||
setIsLoading(false)
|
||
return cachedBitmap
|
||
}
|
||
|
||
try {
|
||
// 清理之前的订阅
|
||
if (unsubscribeRef.current) {
|
||
unsubscribeRef.current()
|
||
}
|
||
|
||
// 订阅帧加载完成通知
|
||
unsubscribeRef.current = subscribeToFrame(frameIndex, (loadedFrameIndex, imageBitmap) => {
|
||
if (loadedFrameIndex === frameIndex) {
|
||
setCurrentBitmap(imageBitmap)
|
||
setIsLoading(false)
|
||
unsubscribeRef.current = null
|
||
}
|
||
})
|
||
|
||
// 使用高优先级加载当前帧
|
||
loadFrame(url, frameIndex, 'high')
|
||
|
||
// 触发预缓存
|
||
await preCacheAroundTime(timestamp)
|
||
|
||
return null // 实际的bitmap会通过订阅通知获取
|
||
} catch (err) {
|
||
const errorMessage = err instanceof Error ? err.message : 'Unknown error'
|
||
setError(errorMessage)
|
||
setIsLoading(false)
|
||
if (unsubscribeRef.current) {
|
||
unsubscribeRef.current()
|
||
unsubscribeRef.current = null
|
||
}
|
||
return null
|
||
}
|
||
}, [generateUrl, getFrameIndex, getFrame, loadFrame, subscribeToFrame])
|
||
|
||
// 在指定时间前后预缓存N个帧
|
||
const preCacheAroundTime = useCallback(async (centerTimestamp: number, timeInterval: number = 360000) => { // 默认5分钟间隔
|
||
if (!generateUrl) return
|
||
|
||
const urlsWithFrameIndices: Array<{url: string, frameIndex: number}> = []
|
||
|
||
// 生成前后各N个时间点的URL和对应的frameIndex
|
||
for (let i = -preCacheCount; i <= preCacheCount; i++) {
|
||
if (i === 0) continue // 跳过当前帧
|
||
|
||
const timestamp = centerTimestamp + (i * timeInterval)
|
||
const url = generateUrl(timestamp)
|
||
const frameIndex = getFrameIndex(timestamp)
|
||
urlsWithFrameIndices.push({ url, frameIndex })
|
||
}
|
||
|
||
// 分别加载每个帧,使用正确的frameIndex
|
||
if (urlsWithFrameIndices.length > 0) {
|
||
urlsWithFrameIndices.forEach(({ url, frameIndex }) => {
|
||
loadFrame(url, frameIndex, 'normal')
|
||
})
|
||
}
|
||
}, [generateUrl, getFrameIndex, preCacheCount, loadFrame])
|
||
|
||
// 清理订阅当组件卸载时
|
||
useEffect(() => {
|
||
return () => {
|
||
if (unsubscribeRef.current) {
|
||
unsubscribeRef.current()
|
||
}
|
||
}
|
||
}, [])
|
||
|
||
// 同步错误和加载状态
|
||
useEffect(() => {
|
||
if (frameLoaderError && !error) {
|
||
setError(frameLoaderError)
|
||
setIsLoading(false)
|
||
}
|
||
}, [frameLoaderError, error])
|
||
|
||
useEffect(() => {
|
||
setIsLoading(frameLoaderIsLoading)
|
||
}, [frameLoaderIsLoading])
|
||
|
||
// 高级功能:清空缓存
|
||
const clearAllCache = useCallback(() => {
|
||
if (unsubscribeRef.current) {
|
||
unsubscribeRef.current()
|
||
unsubscribeRef.current = null
|
||
}
|
||
clearCache()
|
||
timestampToFrameMap.current.clear()
|
||
setCurrentBitmap(null)
|
||
setCurrentFrame(null)
|
||
setError(null)
|
||
setIsLoading(false)
|
||
}, [clearCache])
|
||
|
||
// 高级功能:预缓存指定时间范围
|
||
const preCacheTimeRange = useCallback(async (startTime: number, endTime: number, timeInterval: number = 300000) => {
|
||
if (!generateUrl) return
|
||
|
||
const urlsWithFrameIndices: Array<{url: string, frameIndex: number}> = []
|
||
|
||
for (let timestamp = startTime; timestamp <= endTime; timestamp += timeInterval) {
|
||
const url = generateUrl(timestamp)
|
||
const frameIndex = getFrameIndex(timestamp)
|
||
urlsWithFrameIndices.push({ url, frameIndex })
|
||
}
|
||
|
||
// 分别加载每个帧,使用正确的frameIndex
|
||
if (urlsWithFrameIndices.length > 0) {
|
||
urlsWithFrameIndices.forEach(({ url, frameIndex }) => {
|
||
loadFrame(url, frameIndex, 'normal')
|
||
})
|
||
}
|
||
}, [generateUrl, getFrameIndex, loadFrame])
|
||
|
||
// 高级功能:检查时间是否已缓存
|
||
const isTimeCached = useCallback((timestamp: number): boolean => {
|
||
const frameIndex = getFrameIndex(timestamp)
|
||
return isFrameCached(frameIndex)
|
||
}, [getFrameIndex, isFrameCached])
|
||
|
||
// 高级功能:获取指定时间的帧(不触发加载)
|
||
const getFrameAtTime = useCallback((timestamp: number): ImageBitmap | null => {
|
||
const frameIndex = getFrameIndex(timestamp)
|
||
return getFrame(frameIndex)
|
||
}, [getFrameIndex, getFrame])
|
||
|
||
return {
|
||
// 核心功能:基于时间切换帧
|
||
switchToTime,
|
||
|
||
// 当前状态
|
||
currentFrame,
|
||
currentBitmap,
|
||
currentTimestamp: currentFrame?.timestamp || null,
|
||
|
||
// 加载状态
|
||
isLoading,
|
||
error,
|
||
|
||
// 高级功能
|
||
preCacheAroundTime,
|
||
preCacheTimeRange,
|
||
clearAllCache,
|
||
getCacheStats,
|
||
|
||
// 实用工具
|
||
isTimeCached,
|
||
getFrameAtTime,
|
||
getFrameIndex,
|
||
|
||
// 底层信息
|
||
isWorkerSupported
|
||
}
|
||
} |