mosaicmap/hooks/use-radartile.ts
2025-08-26 11:42:24 +08:00

245 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}