Compare commits
3 Commits
aafc5078fa
...
9646e5d0e1
| Author | SHA1 | Date | |
|---|---|---|---|
| 9646e5d0e1 | |||
| 520efbbb0d | |||
| 06ba5d7ab1 |
@ -1,15 +1,14 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { createContext, useContext, useRef, useState, ReactNode } from 'react'
|
import React, { createContext, useContext, useRef, useState, ReactNode } from 'react'
|
||||||
// import Map from 'ol/Map';
|
|
||||||
import { Map } from 'maplibre-gl';
|
import { Map } from 'maplibre-gl';
|
||||||
import { fromLonLat } from 'ol/proj';
|
|
||||||
|
|
||||||
// 定义MapContext的类型
|
// 定义MapContext的类型
|
||||||
interface MapContextType {
|
interface MapContextType {
|
||||||
mapRef: React.RefObject<Map | null>
|
mapRef: React.RefObject<Map | null>
|
||||||
layers: React.RefObject<any[]>
|
layers: React.RefObject<any[]>
|
||||||
mapState: MapState
|
mapState: MapState
|
||||||
|
currentDatetime: Date | null
|
||||||
setMap: (map: Map, layers: any[]) => void
|
setMap: (map: Map, layers: any[]) => void
|
||||||
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
|
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
|
||||||
zoomIn: () => void
|
zoomIn: () => void
|
||||||
@ -21,6 +20,8 @@ interface MapContextType {
|
|||||||
isMapReady: boolean
|
isMapReady: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 创建Context
|
// 创建Context
|
||||||
const MapContext = createContext<MapContextType | undefined>(undefined)
|
const MapContext = createContext<MapContextType | undefined>(undefined)
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const layersRef = useRef<any[]>([])
|
const layersRef = useRef<any[]>([])
|
||||||
|
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
|
||||||
|
|
||||||
const setMap = (map: Map, layers: any[]) => {
|
const setMap = (map: Map, layers: any[]) => {
|
||||||
// 如果已经有地图实例,先清理旧的
|
// 如果已经有地图实例,先清理旧的
|
||||||
@ -51,9 +52,6 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
mapRef.current = null;
|
mapRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听视图变化事件
|
|
||||||
// const view = map.getView();
|
|
||||||
|
|
||||||
// 监听视图的缩放变化
|
// 监听视图的缩放变化
|
||||||
map.on('zoom', () => {
|
map.on('zoom', () => {
|
||||||
setMapState(prevState => ({
|
setMapState(prevState => ({
|
||||||
@ -78,12 +76,6 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
|
|
||||||
const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => {
|
const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
// mapRef.current.getView().animate({
|
|
||||||
// center: fromLonLat(options.center),
|
|
||||||
// zoom: options.zoom,
|
|
||||||
// duration: options.duration || 1000
|
|
||||||
// })
|
|
||||||
|
|
||||||
mapRef.current.flyTo({
|
mapRef.current.flyTo({
|
||||||
center: options.center,
|
center: options.center,
|
||||||
zoom: options.zoom,
|
zoom: options.zoom,
|
||||||
@ -95,42 +87,28 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
|
|
||||||
const zoomIn = () => {
|
const zoomIn = () => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! + 1)
|
|
||||||
mapRef.current.zoomIn()
|
mapRef.current.zoomIn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoomOut = () => {
|
const zoomOut = () => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! - 1)
|
|
||||||
mapRef.current.zoomOut()
|
mapRef.current.zoomOut()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const zoomTo = (zoom: number) => {
|
const zoomTo = (zoom: number) => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
// mapRef.current.getView().setZoom(zoom)
|
|
||||||
mapRef.current.zoomTo(zoom)
|
mapRef.current.zoomTo(zoom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setTime = (date: Date) => {
|
const setTime = (date: Date) => {
|
||||||
if (mapRef.current) {
|
setCurrentDatetime(date)
|
||||||
layersRef.current.forEach(layer => {
|
|
||||||
const source = layer.getSource()
|
|
||||||
if (source) {
|
|
||||||
source.updateParams({
|
|
||||||
'TIME': date.toISOString()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if (mapRef.current) {
|
if (mapRef.current) {
|
||||||
// mapRef.current.getView().setCenter([103.851959, 1.290270])
|
|
||||||
// mapRef.current.getView().setZoom(11)
|
|
||||||
mapRef.current.flyTo({
|
mapRef.current.flyTo({
|
||||||
center: [103.851959, 1.290270],
|
center: [103.851959, 1.290270],
|
||||||
zoom: 11,
|
zoom: 11,
|
||||||
@ -154,6 +132,7 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
|
|
||||||
const value: MapContextType = {
|
const value: MapContextType = {
|
||||||
setTime,
|
setTime,
|
||||||
|
currentDatetime,
|
||||||
mapRef,
|
mapRef,
|
||||||
layers: layersRef,
|
layers: layersRef,
|
||||||
mapState,
|
mapState,
|
||||||
|
|||||||
41
app/page.tsx
41
app/page.tsx
@ -18,20 +18,8 @@ import { ThemeToggle } from '@/components/theme-toggle';
|
|||||||
// import { Timeline } from '@/app/timeline';
|
// import { Timeline } from '@/app/timeline';
|
||||||
import { Timeline } from '@/app/tl';
|
import { Timeline } from '@/app/tl';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTimeline } from '@/hooks/use-timeline';
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useRadarTile } from '@/hooks/use-radartile'
|
|
||||||
import { gql, useSubscription } from '@apollo/client'
|
|
||||||
|
|
||||||
const SUBSCRIPTION_QUERY = gql`
|
|
||||||
subscription {
|
|
||||||
statusUpdates {
|
|
||||||
id
|
|
||||||
message
|
|
||||||
status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
|
||||||
@ -39,16 +27,6 @@ export default function Page() {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
|
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
|
||||||
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
|
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
|
||||||
const { setTime } = useTimeline()
|
|
||||||
const { imgBitmap, fetchRadarTile } = useRadarTile({})
|
|
||||||
|
|
||||||
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
console.log(data.statusUpdates)
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
@ -62,22 +40,7 @@ export default function Page() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="relative h-full w-full flex flex-col">
|
<div className="relative h-full w-full flex flex-col">
|
||||||
<MapComponent imgBitmap={imgBitmap} />
|
<MapComponent />
|
||||||
{/* <Timeline
|
|
||||||
className={
|
|
||||||
cn(
|
|
||||||
"backdrop-blur-lg border shadow-lg",
|
|
||||||
"bg-background/90 border-border",
|
|
||||||
"z-10"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
onDateChange={(date) => {
|
|
||||||
console.log('Selected date:', date);
|
|
||||||
setTime(date)
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
<Timeline />
|
<Timeline />
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|||||||
@ -133,7 +133,6 @@ function SelectDemo() {
|
|||||||
export const Timeline: React.FC<Props> = ({
|
export const Timeline: React.FC<Props> = ({
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
currentDate,
|
|
||||||
onDateChange,
|
onDateChange,
|
||||||
onPlay,
|
onPlay,
|
||||||
onPause,
|
onPause,
|
||||||
@ -148,19 +147,12 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const { isPlaying, togglePlay, loading, setTime, currentDatetime: currentDate } = useTimeline({})
|
||||||
const { isPlaying, togglePlay } = useTimeline({
|
|
||||||
initialDate: currentDate ?? new Date(),
|
|
||||||
onDateChange(date) {
|
|
||||||
onDateChange?.(date);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const [state, setState] = useState<Status>({
|
const [state, setState] = useState<Status>({
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isLongPress: false,
|
isLongPress: false,
|
||||||
isPanningTimeline: false,
|
isPanningTimeline: false,
|
||||||
customLineTimestamp: currentDate?.getTime() ?? new Date().getTime(),
|
customLineTimestamp: currentDate?.getTime() ?? null,
|
||||||
panOffset: 0,
|
panOffset: 0,
|
||||||
zoomLevel: initialZoom,
|
zoomLevel: initialZoom,
|
||||||
});
|
});
|
||||||
@ -188,6 +180,7 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
const newDate = new Date(state.customLineTimestamp! + 360000);
|
const newDate = new Date(state.customLineTimestamp! + 360000);
|
||||||
setState({ ...state, customLineTimestamp: newDate.getTime() });
|
setState({ ...state, customLineTimestamp: newDate.getTime() });
|
||||||
onDateChange?.(newDate);
|
onDateChange?.(newDate);
|
||||||
|
setTime(newDate);
|
||||||
}, [state.customLineTimestamp])
|
}, [state.customLineTimestamp])
|
||||||
|
|
||||||
// 缩放处理函数
|
// 缩放处理函数
|
||||||
@ -282,7 +275,7 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
dpr,
|
dpr,
|
||||||
overrides.startDate ?? currentProps.startDate,
|
overrides.startDate ?? currentProps.startDate,
|
||||||
overrides.endDate ?? currentProps.endDate,
|
overrides.endDate ?? currentProps.endDate,
|
||||||
overrides.currentDate ?? currentProps.currentDate,
|
overrides.currentDate ?? currentProps.currentDate ?? undefined,
|
||||||
overrides.zoomLevel ?? currentState.zoomLevel,
|
overrides.zoomLevel ?? currentState.zoomLevel,
|
||||||
overrides.panOffset ?? currentState.panOffset,
|
overrides.panOffset ?? currentState.panOffset,
|
||||||
overrides.customLineTimestamp ?? currentState.customLineTimestamp,
|
overrides.customLineTimestamp ?? currentState.customLineTimestamp,
|
||||||
@ -386,17 +379,24 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const selectedTimestamp = visibleStartTime + progress * visibleTimeRange;
|
const selectedTimestamp = visibleStartTime + progress * visibleTimeRange;
|
||||||
|
|
||||||
|
// 规整到最近的6分钟整数时间
|
||||||
|
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
|
||||||
|
const roundedTimestamp = Math.round(selectedTimestamp / sixMinutesInMs) * sixMinutesInMs;
|
||||||
|
|
||||||
setState(prevState => ({
|
setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
customLineTimestamp: selectedTimestamp,
|
customLineTimestamp: roundedTimestamp,
|
||||||
isLongPress: false,
|
isLongPress: false,
|
||||||
isDragging: false
|
isDragging: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 通知父组件
|
// 通知父组件
|
||||||
if (onDateChange) {
|
if (onDateChange) {
|
||||||
onDateChange(new Date(selectedTimestamp));
|
onDateChange(new Date(roundedTimestamp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用setTime更新时间轴状态
|
||||||
|
setTime(new Date(roundedTimestamp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -601,12 +601,19 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
|
|
||||||
const finalTimestamp = visibleStartTime + progress * visibleTimeRange;
|
const finalTimestamp = visibleStartTime + progress * visibleTimeRange;
|
||||||
|
|
||||||
|
// 规整到最近的6分钟整数时间
|
||||||
|
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
|
||||||
|
const roundedFinalTimestamp = Math.round(finalTimestamp / sixMinutesInMs) * sixMinutesInMs;
|
||||||
|
|
||||||
setState(prevState => ({
|
setState(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
customLineTimestamp: finalTimestamp,
|
customLineTimestamp: roundedFinalTimestamp,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
isLongPress: false
|
isLongPress: false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// 使用setTime更新时间轴状态
|
||||||
|
setTime(new Date(roundedFinalTimestamp));
|
||||||
} else {
|
} else {
|
||||||
setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false }));
|
setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false }));
|
||||||
}
|
}
|
||||||
@ -706,9 +713,6 @@ export const Timeline: React.FC<Props> = ({
|
|||||||
current_uniforms.current.currentTimestamp = currentDate.getTime();
|
current_uniforms.current.currentTimestamp = currentDate.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 鼠标滚轮缩放和平移
|
// 鼠标滚轮缩放和平移
|
||||||
const handleWheel = (e: WheelEvent) => {
|
const handleWheel = (e: WheelEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
72
app/tl.tsx
72
app/tl.tsx
@ -3,12 +3,26 @@ import vsSource from './glsl/timeline/vert.glsl';
|
|||||||
import fsSource from './glsl/timeline/frag.glsl';
|
import fsSource from './glsl/timeline/frag.glsl';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ChevronLeft, ChevronRight, HomeIcon, Pause, Play } from "lucide-react";
|
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, UnlockIcon } from "lucide-react";
|
||||||
|
import { formatInTimeZone } from "date-fns-tz";
|
||||||
|
import { parse } from "date-fns"
|
||||||
|
|
||||||
import { useTimeline } from "@/hooks/use-timeline";
|
import { useTimeline } from "@/hooks/use-timeline";
|
||||||
import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline";
|
import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { gql, useSubscription } from "@apollo/client";
|
||||||
|
|
||||||
|
const SUBSCRIPTION_QUERY = gql`
|
||||||
|
subscription {
|
||||||
|
statusUpdates {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
status
|
||||||
|
timestamp
|
||||||
|
newestDt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
interface Uniforms {
|
interface Uniforms {
|
||||||
startTimestamp: number; // Unix 时间戳开始
|
startTimestamp: number; // Unix 时间戳开始
|
||||||
@ -65,7 +79,8 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
timelineConfig,
|
timelineConfig,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { isPlaying, togglePlay } = useTimeline({})
|
const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
|
||||||
|
const [lock, setLock] = useState(false)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const timelineEngineRef = useRef<TimelineEngine | null>(null);
|
const timelineEngineRef = useRef<TimelineEngine | null>(null);
|
||||||
@ -77,6 +92,21 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
currentLevel: null as any
|
currentLevel: null as any
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
if (data.statusUpdates) {
|
||||||
|
if (!lock && data.statusUpdates.newestDt) {
|
||||||
|
const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date())
|
||||||
|
setTime(newDt)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data, lock])
|
||||||
|
|
||||||
// 定时器效果 - 当播放时每隔指定时间执行操作
|
// 定时器效果 - 当播放时每隔指定时间执行操作
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
let intervalId: NodeJS.Timeout | null = null;
|
||||||
@ -85,7 +115,6 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
intervalId = setInterval(() => {
|
intervalId = setInterval(() => {
|
||||||
// 执行时间前进操作
|
// 执行时间前进操作
|
||||||
if (timelineEngineRef.current) {
|
if (timelineEngineRef.current) {
|
||||||
// timelineEngineRef.current.forwardTimeMark(timeStep);
|
|
||||||
timelineEngineRef.current.playAndEnsureMarkInView(timeStep)
|
timelineEngineRef.current.playAndEnsureMarkInView(timeStep)
|
||||||
}
|
}
|
||||||
}, 1000); // 每秒执行一次,你可以根据需要调整这个间隔
|
}, 1000); // 每秒执行一次,你可以根据需要调整这个间隔
|
||||||
@ -98,6 +127,12 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
};
|
};
|
||||||
}, [isPlaying, timeStep]);
|
}, [isPlaying, timeStep]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentDatetime && !lock) {
|
||||||
|
timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime())
|
||||||
|
}
|
||||||
|
}, [currentDatetime, lock])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ticksCanvasRef.current) return;
|
if (!ticksCanvasRef.current) return;
|
||||||
|
|
||||||
@ -132,6 +167,20 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
primaryFontSize: 10,
|
primaryFontSize: 10,
|
||||||
secondaryFontSize: 10
|
secondaryFontSize: 10
|
||||||
},
|
},
|
||||||
|
onDateChange: async (date: Date) => {
|
||||||
|
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
|
||||||
|
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
const nearestDatetime = data.nearest_data_time
|
||||||
|
const nearestDate = new Date(Date.parse(nearestDatetime))
|
||||||
|
|
||||||
|
setTime(nearestDate)
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch data:', response.status)
|
||||||
|
}
|
||||||
|
},
|
||||||
...timelineConfig
|
...timelineConfig
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -210,7 +259,10 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="size-5"
|
className="size-5"
|
||||||
onClick={() => togglePlay()}
|
onClick={() => {
|
||||||
|
togglePlay()
|
||||||
|
setLock(true)
|
||||||
|
}}
|
||||||
title={isPlaying ? "暂停" : "播放"}
|
title={isPlaying ? "暂停" : "播放"}
|
||||||
>
|
>
|
||||||
{isPlaying ? <Pause size={10} /> : <Play size={10} />}
|
{isPlaying ? <Pause size={10} /> : <Play size={10} />}
|
||||||
@ -260,6 +312,16 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="size-5"
|
||||||
|
onClick={() => setLock(!lock)}
|
||||||
|
title="锁定时间"
|
||||||
|
>
|
||||||
|
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import { useMapLocation } from '@/hooks/use-map-location'
|
|||||||
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
|
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
|
||||||
import { createColorMap, ColorMapType, } from '@/lib/color-maps'
|
import { createColorMap, ColorMapType, } from '@/lib/color-maps'
|
||||||
import { Colorbar } from './colorbar'
|
import { Colorbar } from './colorbar'
|
||||||
|
import { useRadarTile } from '@/hooks/use-radartile'
|
||||||
|
import { format, formatInTimeZone } from 'date-fns-tz'
|
||||||
|
|
||||||
interface MapComponentProps {
|
interface MapComponentProps {
|
||||||
style?: string
|
style?: string
|
||||||
@ -24,13 +26,14 @@ export function MapComponent({
|
|||||||
// center = [103.851959, 1.290270],
|
// center = [103.851959, 1.290270],
|
||||||
// zoom = 11
|
// zoom = 11
|
||||||
imgBitmap: propImgBitmap,
|
imgBitmap: propImgBitmap,
|
||||||
colorMapType = 'heatmap',
|
colorMapType = 'meteorological',
|
||||||
onColorMapChange
|
onColorMapChange
|
||||||
}: MapComponentProps) {
|
}: MapComponentProps) {
|
||||||
|
|
||||||
|
const { fetchRadarTile, imgBitmap } = useRadarTile();
|
||||||
const mapContainer = useRef<HTMLDivElement>(null)
|
const mapContainer = useRef<HTMLDivElement>(null)
|
||||||
const { setMap } = useMap()
|
const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
|
||||||
const { location } = useMapLocation()
|
const { location } = useMapLocation()
|
||||||
const imgBitmap = propImgBitmap
|
|
||||||
const texRef = useRef<WebGLTexture | null>(null)
|
const texRef = useRef<WebGLTexture | null>(null)
|
||||||
const lutTexRef = useRef<WebGLTexture | null>(null)
|
const lutTexRef = useRef<WebGLTexture | null>(null)
|
||||||
const glRef = useRef<WebGL2RenderingContext | null>(null)
|
const glRef = useRef<WebGL2RenderingContext | null>(null)
|
||||||
@ -38,6 +41,13 @@ export function MapComponent({
|
|||||||
const [isReady, setIsReady] = useState<boolean>(false)
|
const [isReady, setIsReady] = useState<boolean>(false)
|
||||||
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
|
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMapReady || !currentDatetime) return;
|
||||||
|
const utc_time_str = formatInTimeZone(currentDatetime, 'UTC', 'yyyyMMddHHmmss')
|
||||||
|
const new_url = `http://localhost:3050/api/v1/data?datetime=${utc_time_str}&area=cn`
|
||||||
|
fetchRadarTile(new_url)
|
||||||
|
}, [currentDatetime, isMapReady])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mapContainer.current) return
|
if (!mapContainer.current) return
|
||||||
|
|
||||||
@ -101,8 +111,9 @@ export function MapComponent({
|
|||||||
// 对于灰度图,RGB通道通常相同,取红色通道作为灰度值
|
// 对于灰度图,RGB通道通常相同,取红色通道作为灰度值
|
||||||
float value = texColor.r * 3.4;
|
float value = texColor.r * 3.4;
|
||||||
|
|
||||||
if (value == 0.0) {
|
if (value < 0.07) {
|
||||||
discard;
|
fragColor= vec4(1.0,1.0,1.0,0.2);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
|
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
|
||||||
@ -111,8 +122,9 @@ export function MapComponent({
|
|||||||
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
|
||||||
// 添加一些透明度,使低值区域更透明
|
// 添加一些透明度,使低值区域更透明
|
||||||
// float alpha = smoothstep(0.0, 0.1, value);
|
// float alpha = smoothstep(0.0, 0.1, value);
|
||||||
float alpha = 1.0;
|
float alpha = 0.7;
|
||||||
fragColor = vec4(lutColor.rgb, alpha);
|
fragColor = vec4(lutColor.rgb, alpha);
|
||||||
|
// fragColor = vec4(1.0,1.0,1.0,0.2);
|
||||||
}`
|
}`
|
||||||
|
|
||||||
console.log(vertexSource, fragmentSource)
|
console.log(vertexSource, fragmentSource)
|
||||||
@ -166,8 +178,8 @@ export function MapComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
@ -463,6 +475,10 @@ export function MapComponent({
|
|||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||||
|
|
||||||
|
// Redraw the map
|
||||||
|
mapRef.current?.triggerRepaint()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [imgBitmap, isReady])
|
}, [imgBitmap, isReady])
|
||||||
@ -556,8 +572,8 @@ function createLutTexture(gl: WebGL2RenderingContext, colorMapType: ColorMapType
|
|||||||
lut
|
lut
|
||||||
)
|
)
|
||||||
|
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,8 @@ export function Timeline({
|
|||||||
const newDays = Math.round((newProgress / 100) * totalDays)
|
const newDays = Math.round((newProgress / 100) * totalDays)
|
||||||
const newDate = addDays(startDate, newDays)
|
const newDate = addDays(startDate, newDays)
|
||||||
onDateChange?.(newDate)
|
onDateChange?.(newDate)
|
||||||
}, [startDate, totalDays, onDateChange])
|
timeline.setTime(newDate)
|
||||||
|
}, [startDate, totalDays, onDateChange, timeline])
|
||||||
|
|
||||||
const handlePlayPause = useCallback(() => {
|
const handlePlayPause = useCallback(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
@ -82,12 +83,14 @@ export function Timeline({
|
|||||||
const handleSkipBack = useCallback(() => {
|
const handleSkipBack = useCallback(() => {
|
||||||
const newDate = subDays(currentDate, 1)
|
const newDate = subDays(currentDate, 1)
|
||||||
onDateChange?.(newDate)
|
onDateChange?.(newDate)
|
||||||
}, [currentDate, onDateChange])
|
timeline.setTime(newDate)
|
||||||
|
}, [currentDate, onDateChange, timeline])
|
||||||
|
|
||||||
const handleSkipForward = useCallback(() => {
|
const handleSkipForward = useCallback(() => {
|
||||||
const newDate = addDays(currentDate, 1)
|
const newDate = addDays(currentDate, 1)
|
||||||
onDateChange?.(newDate)
|
onDateChange?.(newDate)
|
||||||
}, [currentDate, onDateChange])
|
timeline.setTime(newDate)
|
||||||
|
}, [currentDate, onDateChange, timeline])
|
||||||
|
|
||||||
const speedOptions = [
|
const speedOptions = [
|
||||||
{ value: 'slow', label: '慢速', interval: 2000 },
|
{ value: 'slow', label: '慢速', interval: 2000 },
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const LOCATIONS = {
|
|||||||
export type LocationKey = keyof typeof LOCATIONS
|
export type LocationKey = keyof typeof LOCATIONS
|
||||||
|
|
||||||
export function useMapLocation() {
|
export function useMapLocation() {
|
||||||
const [currentLocation, setCurrentLocation] = useState<LocationKey>('usa')
|
const [currentLocation, setCurrentLocation] = useState<LocationKey>('china')
|
||||||
const { flyTo, isMapReady } = useMap()
|
const { flyTo, isMapReady } = useMap()
|
||||||
|
|
||||||
const flyToLocation = useCallback((location: LocationKey) => {
|
const flyToLocation = useCallback((location: LocationKey) => {
|
||||||
|
|||||||
@ -1,28 +1,11 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { addDays, subDays } from 'date-fns'
|
import { addDays, subDays } from 'date-fns'
|
||||||
import { useMap } from '@/app/map-context'
|
import { useMap } from '@/app/map-context'
|
||||||
|
import { useSubscription, gql } from '@apollo/client'
|
||||||
|
import { parse } from 'date-fns'
|
||||||
|
import { UTCDate } from "@date-fns/utc";
|
||||||
|
import { toZonedTime } from 'date-fns-tz';
|
||||||
|
|
||||||
interface UseTimelineOptions {
|
|
||||||
startDate?: Date
|
|
||||||
endDate?: Date
|
|
||||||
initialDate?: Date
|
|
||||||
onDateChange?: (date: Date) => void
|
|
||||||
autoPlay?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTimeline({
|
|
||||||
startDate = subDays(new Date(), 30),
|
|
||||||
endDate = new Date(),
|
|
||||||
initialDate = new Date(),
|
|
||||||
onDateChange,
|
|
||||||
autoPlay = false
|
|
||||||
}: UseTimelineOptions = {}) {
|
|
||||||
const [currentDate, setCurrentDate] = useState(initialDate)
|
|
||||||
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
|
||||||
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
|
||||||
|
|
||||||
const { setTime } = useMap()
|
|
||||||
|
|
||||||
const speedIntervals = {
|
const speedIntervals = {
|
||||||
slow: 2000,
|
slow: 2000,
|
||||||
@ -30,11 +13,35 @@ export function useTimeline({
|
|||||||
fast: 500
|
fast: 500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface UseTimelineOptions {
|
||||||
|
startDate?: Date
|
||||||
|
endDate?: Date
|
||||||
|
initialDate?: Date
|
||||||
|
onDateChange?: (date: Date) => void
|
||||||
|
autoPlay?: boolean
|
||||||
|
autoUpdate?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTimeline({
|
||||||
|
startDate = subDays(new Date(), 30),
|
||||||
|
endDate = new Date(),
|
||||||
|
onDateChange,
|
||||||
|
autoPlay = false
|
||||||
|
}: UseTimelineOptions = {}) {
|
||||||
|
|
||||||
|
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
||||||
|
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
|
const { setTime, currentDatetime } = useMap()
|
||||||
|
|
||||||
const updateDate = useCallback((newDate: Date) => {
|
const updateDate = useCallback((newDate: Date) => {
|
||||||
setCurrentDate(newDate)
|
setTime(newDate)
|
||||||
onDateChange?.(newDate)
|
onDateChange?.(newDate)
|
||||||
}, [onDateChange])
|
}, [onDateChange])
|
||||||
|
|
||||||
|
|
||||||
const play = useCallback(() => {
|
const play = useCallback(() => {
|
||||||
setIsPlaying(true)
|
setIsPlaying(true)
|
||||||
}, [])
|
}, [])
|
||||||
@ -48,18 +55,20 @@ export function useTimeline({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const skipForward = useCallback(() => {
|
const skipForward = useCallback(() => {
|
||||||
const newDate = addDays(currentDate, 1)
|
if (!currentDatetime) return;
|
||||||
|
const newDate = addDays(currentDatetime, 1)
|
||||||
if (newDate <= endDate) {
|
if (newDate <= endDate) {
|
||||||
updateDate(newDate)
|
updateDate(newDate)
|
||||||
}
|
}
|
||||||
}, [currentDate, endDate, updateDate])
|
}, [currentDatetime, endDate, updateDate])
|
||||||
|
|
||||||
const skipBackward = useCallback(() => {
|
const skipBackward = useCallback(() => {
|
||||||
const newDate = subDays(currentDate, 1)
|
if (!currentDatetime) return;
|
||||||
|
const newDate = subDays(currentDatetime, 1)
|
||||||
if (newDate >= startDate) {
|
if (newDate >= startDate) {
|
||||||
updateDate(newDate)
|
updateDate(newDate)
|
||||||
}
|
}
|
||||||
}, [currentDate, startDate, updateDate])
|
}, [currentDatetime, startDate, updateDate])
|
||||||
|
|
||||||
const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => {
|
const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => {
|
||||||
setSpeed(newSpeed)
|
setSpeed(newSpeed)
|
||||||
@ -75,7 +84,8 @@ export function useTimeline({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
intervalRef.current = setInterval(() => {
|
intervalRef.current = setInterval(() => {
|
||||||
const nextDate = addDays(currentDate, 1)
|
if (!currentDatetime) return;
|
||||||
|
const nextDate = addDays(currentDatetime, 1)
|
||||||
if (nextDate <= endDate) {
|
if (nextDate <= endDate) {
|
||||||
updateDate(nextDate)
|
updateDate(nextDate)
|
||||||
} else {
|
} else {
|
||||||
@ -95,7 +105,7 @@ export function useTimeline({
|
|||||||
clearInterval(intervalRef.current)
|
clearInterval(intervalRef.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isPlaying, currentDate, endDate, speed, updateDate])
|
}, [isPlaying, currentDatetime, endDate, speed, updateDate])
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -107,7 +117,7 @@ export function useTimeline({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentDate,
|
currentDatetime,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
speed,
|
speed,
|
||||||
startDate,
|
startDate,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// 色标函数集合
|
// 色标函数集合
|
||||||
export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale';
|
export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale' | 'meteorological';
|
||||||
|
|
||||||
// 雷达色标 (深蓝到红色,类似气象雷达)
|
// 雷达色标 (深蓝到红色,类似气象雷达)
|
||||||
export function createRadarColorMap(): Uint8Array {
|
export function createRadarColorMap(): Uint8Array {
|
||||||
@ -207,11 +207,109 @@ export function createColorMap(type: ColorMapType): Uint8Array {
|
|||||||
return createPlasmaColorMap();
|
return createPlasmaColorMap();
|
||||||
case 'grayscale':
|
case 'grayscale':
|
||||||
return createGrayscaleColorMap();
|
return createGrayscaleColorMap();
|
||||||
|
case 'meteorological':
|
||||||
|
return createMeteorologicalColorMap();
|
||||||
default:
|
default:
|
||||||
return createRadarColorMap();
|
return createRadarColorMap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// COLOR_MAP = [
|
||||||
|
// ("#01a0f6", (5, 10)),
|
||||||
|
// ("#00ecec", (10, 15)),
|
||||||
|
// ("#6dfa3d", (15, 20)),
|
||||||
|
// ("#00d802", (20, 25)),
|
||||||
|
// ("#019001", (25, 30)),
|
||||||
|
// ("#ffff04", (30, 35)),
|
||||||
|
// ("#e7c002", (35, 40)),
|
||||||
|
// ("#ff9002", (40, 45)),
|
||||||
|
// ("#ff0201", (45, 50)),
|
||||||
|
// ("#d60101", (50, 55)),
|
||||||
|
// ("#c00100", (55, 60)),
|
||||||
|
// ("#ff00f0", (60, 65)),
|
||||||
|
// ("#9600b4", (65, 70)),
|
||||||
|
// ("#ad90f0", (70, 75)),
|
||||||
|
// ]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 根据具体数值区间创建的气象雷达色标
|
||||||
|
export function createMeteorologicalColorMap(): Uint8Array {
|
||||||
|
const lut = new Uint8Array(256 * 4);
|
||||||
|
|
||||||
|
// 定义颜色和数值区间对应关系
|
||||||
|
const colorRanges: Array<{ color: [number, number, number]; range: [number, number] }> = [
|
||||||
|
{ color: [1, 160, 246], range: [5, 10] }, // #01a0f6
|
||||||
|
{ color: [0, 236, 236], range: [10, 15] }, // #00ecec
|
||||||
|
{ color: [109, 250, 61], range: [15, 20] }, // #6dfa3d
|
||||||
|
{ color: [0, 216, 2], range: [20, 25] }, // #00d802
|
||||||
|
{ color: [1, 144, 1], range: [25, 30] }, // #019001
|
||||||
|
{ color: [255, 255, 4], range: [30, 35] }, // #ffff04 (黄色,重要节点)
|
||||||
|
{ color: [231, 192, 2], range: [35, 40] }, // #e7c002
|
||||||
|
{ color: [255, 144, 2], range: [40, 45] }, // #ff9002
|
||||||
|
{ color: [255, 2, 1], range: [45, 50] }, // #ff0201
|
||||||
|
{ color: [214, 1, 1], range: [50, 55] }, // #d60101
|
||||||
|
{ color: [192, 1, 0], range: [55, 60] }, // #c00100
|
||||||
|
{ color: [255, 0, 240], range: [60, 65] }, // #ff00f0 (紫色,重要节点)
|
||||||
|
{ color: [150, 0, 180], range: [65, 70] }, // #9600b4
|
||||||
|
{ color: [173, 144, 240], range: [70, 75] } // #ad90f0
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
// 将0-255映射到0-75的数值范围
|
||||||
|
const value = (i / 255.0) * 75;
|
||||||
|
|
||||||
|
let r = 0, g = 0, b = 0;
|
||||||
|
|
||||||
|
// 找到对应的颜色区间
|
||||||
|
let found = false;
|
||||||
|
for (let j = 0; j < colorRanges.length; j++) {
|
||||||
|
const range = colorRanges[j];
|
||||||
|
if (value >= range.range[0] && value <= range.range[1]) {
|
||||||
|
// 在区间内进行线性插值
|
||||||
|
const localT = (value - range.range[0]) / (range.range[1] - range.range[0]);
|
||||||
|
|
||||||
|
if (j < colorRanges.length - 1) {
|
||||||
|
const nextRange = colorRanges[j + 1];
|
||||||
|
// 与下一个颜色进行插值
|
||||||
|
r = Math.floor(range.color[0] + localT * (nextRange.color[0] - range.color[0]));
|
||||||
|
g = Math.floor(range.color[1] + localT * (nextRange.color[1] - range.color[1]));
|
||||||
|
b = Math.floor(range.color[2] + localT * (nextRange.color[2] - range.color[2]));
|
||||||
|
} else {
|
||||||
|
// 最后一个区间,使用固定颜色
|
||||||
|
r = range.color[0];
|
||||||
|
g = range.color[1];
|
||||||
|
b = range.color[2];
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到对应区间,使用最近的颜色
|
||||||
|
if (!found) {
|
||||||
|
if (value < 5) {
|
||||||
|
const firstRange = colorRanges[0]!;
|
||||||
|
r = firstRange.color[0];
|
||||||
|
g = firstRange.color[1];
|
||||||
|
b = firstRange.color[2];
|
||||||
|
} else {
|
||||||
|
const lastRange = colorRanges[colorRanges.length - 1]!;
|
||||||
|
r = lastRange.color[0];
|
||||||
|
g = lastRange.color[1];
|
||||||
|
b = lastRange.color[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lut[i * 4] = r;
|
||||||
|
lut[i * 4 + 1] = g;
|
||||||
|
lut[i * 4 + 2] = b;
|
||||||
|
lut[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lut;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取所有可用的色标类型
|
// 获取所有可用的色标类型
|
||||||
export function getAvailableColorMaps(): { value: ColorMapType; label: string }[] {
|
export function getAvailableColorMaps(): { value: ColorMapType; label: string }[] {
|
||||||
return [
|
return [
|
||||||
@ -220,6 +318,7 @@ export function getAvailableColorMaps(): { value: ColorMapType; label: string }[
|
|||||||
{ value: 'heatmap', label: '热力图色标' },
|
{ value: 'heatmap', label: '热力图色标' },
|
||||||
{ value: 'viridis', label: 'Viridis色标' },
|
{ value: 'viridis', label: 'Viridis色标' },
|
||||||
{ value: 'plasma', label: 'Plasma色标' },
|
{ value: 'plasma', label: 'Plasma色标' },
|
||||||
{ value: 'grayscale', label: '灰度色标' }
|
{ value: 'grayscale', label: '灰度色标' },
|
||||||
|
{ value: 'meteorological', label: '气象雷达色标' }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -3,6 +3,8 @@
|
|||||||
* 用于globe模式下的球面渲染
|
* 用于globe模式下的球面渲染
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MercatorCoordinate } from 'maplibre-gl'
|
||||||
|
|
||||||
export interface TileMeshOptions {
|
export interface TileMeshOptions {
|
||||||
/** 经纬度边界 [west, south, east, north] */
|
/** 经纬度边界 [west, south, east, north] */
|
||||||
bounds: [number, number, number, number];
|
bounds: [number, number, number, number];
|
||||||
@ -163,10 +165,12 @@ export function detectPerformanceLevel(): PerformanceLevel {
|
|||||||
* 将经纬度转换为Web Mercator坐标 (归一化到0-1范围)
|
* 将经纬度转换为Web Mercator坐标 (归一化到0-1范围)
|
||||||
*/
|
*/
|
||||||
function lonLatToMercator(lon: number, lat: number): [number, number] {
|
function lonLatToMercator(lon: number, lat: number): [number, number] {
|
||||||
const x = (lon + 180) / 360;
|
const mercator = MercatorCoordinate.fromLngLat({ lng: lon, lat: lat })
|
||||||
const latRad = (lat * Math.PI) / 180;
|
return [mercator.x, mercator.y]
|
||||||
const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
|
// const x = (lon + 180) / 360;
|
||||||
return [x, y];
|
// const latRad = (lat * Math.PI) / 180;
|
||||||
|
// const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
|
||||||
|
// return [x, y];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -110,6 +110,8 @@ interface TimelineConfig {
|
|||||||
highlightWeekends?: boolean;
|
highlightWeekends?: boolean;
|
||||||
/** 时间标记列表 */
|
/** 时间标记列表 */
|
||||||
timeMarks?: TimeMark[];
|
timeMarks?: TimeMark[];
|
||||||
|
|
||||||
|
onDateChange?: (date: Date) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 时间工具类 */
|
/** 时间工具类 */
|
||||||
@ -649,7 +651,8 @@ class RealTimeTimeline {
|
|||||||
},
|
},
|
||||||
showCurrentTime: true,
|
showCurrentTime: true,
|
||||||
highlightWeekends: true,
|
highlightWeekends: true,
|
||||||
timeMarks: []
|
timeMarks: [],
|
||||||
|
onDateChange: () => { }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!config) return defaultConfig;
|
if (!config) return defaultConfig;
|
||||||
@ -664,7 +667,8 @@ class RealTimeTimeline {
|
|||||||
sizes: { ...defaultConfig.sizes, ...config.sizes },
|
sizes: { ...defaultConfig.sizes, ...config.sizes },
|
||||||
showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime,
|
showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime,
|
||||||
highlightWeekends: config.highlightWeekends ?? defaultConfig.highlightWeekends,
|
highlightWeekends: config.highlightWeekends ?? defaultConfig.highlightWeekends,
|
||||||
timeMarks: config.timeMarks ?? defaultConfig.timeMarks
|
timeMarks: config.timeMarks ?? defaultConfig.timeMarks,
|
||||||
|
onDateChange: config.onDateChange ?? defaultConfig.onDateChange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -723,13 +727,7 @@ class RealTimeTimeline {
|
|||||||
// 获取当前刻度信息用于吸附
|
// 获取当前刻度信息用于吸附
|
||||||
const ticks = this.scaleManager.calculateTicks(this.viewport);
|
const ticks = this.scaleManager.calculateTicks(this.viewport);
|
||||||
const date = this.viewport.screenToTime(x, true, ticks);
|
const date = this.viewport.screenToTime(x, true, ticks);
|
||||||
this.interaction.setZoomMode(ZoomMode.MarkMode);
|
this.changeTime(new Date(date))
|
||||||
this.replaceTimeMark({
|
|
||||||
timestamp: date,
|
|
||||||
color: '#ff6b6b',
|
|
||||||
label: format(date, 'HH:mm:ss'),
|
|
||||||
type: 'custom'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -814,6 +812,17 @@ class RealTimeTimeline {
|
|||||||
this.drawTimeMarks();
|
this.drawTimeMarks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private changeTime(date: Date): void {
|
||||||
|
this.interaction.setZoomMode(ZoomMode.MarkMode);
|
||||||
|
this.replaceTimeMark({
|
||||||
|
timestamp: date.getTime(),
|
||||||
|
color: '#ff6b6b',
|
||||||
|
label: format(date, 'HH:mm:ss'),
|
||||||
|
type: 'custom'
|
||||||
|
});
|
||||||
|
this.config.onDateChange?.(date)
|
||||||
|
}
|
||||||
|
|
||||||
/** 绘制周末高亮 */
|
/** 绘制周末高亮 */
|
||||||
private drawWeekends(): void {
|
private drawWeekends(): void {
|
||||||
const [startTime, endTime] = this.viewport.getVisibleRange();
|
const [startTime, endTime] = this.viewport.getVisibleRange();
|
||||||
@ -1038,19 +1047,25 @@ class RealTimeTimeline {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceTimeMarkByTimestamp(timestamp: number): void {
|
||||||
|
|
||||||
|
this.replaceTimeMark({
|
||||||
|
timestamp: timestamp,
|
||||||
|
color: '#ff6b6b',
|
||||||
|
label: format(timestamp, 'HH:mm:ss'),
|
||||||
|
type: 'custom'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.interaction.setZoomMode(ZoomMode.MarkMode)
|
||||||
|
}
|
||||||
|
|
||||||
forwardTimeMark(delta: number, timestamp?: number): void {
|
forwardTimeMark(delta: number, timestamp?: number): void {
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
timestamp = this.config.timeMarks[0].timestamp;
|
timestamp = this.config.timeMarks[0].timestamp;
|
||||||
}
|
}
|
||||||
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
|
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.config.timeMarks.splice(index, 1);
|
this.changeTime(new Date(timestamp + delta))
|
||||||
this.replaceTimeMark({
|
|
||||||
timestamp: timestamp + delta,
|
|
||||||
color: '#ff6b6b',
|
|
||||||
label: format(timestamp + delta, 'HH:mm:ss'),
|
|
||||||
type: 'custom'
|
|
||||||
});
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1061,13 +1076,7 @@ class RealTimeTimeline {
|
|||||||
}
|
}
|
||||||
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
|
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.config.timeMarks.splice(index, 1);
|
this.changeTime(new Date(timestamp - delta))
|
||||||
this.replaceTimeMark({
|
|
||||||
timestamp: timestamp - delta,
|
|
||||||
color: '#ff6b6b',
|
|
||||||
label: format(timestamp - delta, 'HH:mm:ss'),
|
|
||||||
type: 'custom'
|
|
||||||
});
|
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
package-lock.json
generated
33
package-lock.json
generated
@ -11,6 +11,7 @@
|
|||||||
"@21st-extension/react": "^0.5.14",
|
"@21st-extension/react": "^0.5.14",
|
||||||
"@21st-extension/toolbar-next": "^0.5.14",
|
"@21st-extension/toolbar-next": "^0.5.14",
|
||||||
"@apollo/client": "^3.13.9",
|
"@apollo/client": "^3.13.9",
|
||||||
|
"@date-fns/utc": "^2.1.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@ -37,6 +38,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"dnd-kit": "^0.0.2",
|
"dnd-kit": "^0.0.2",
|
||||||
"framer-motion": "^12.23.6",
|
"framer-motion": "^12.23.6",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
@ -196,6 +198,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
||||||
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
|
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@date-fns/utc": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/@date-fns/utc/-/utc-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@dnd-kit/accessibility": {
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
@ -3254,8 +3262,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/date-fns": {
|
"node_modules/date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@ -3266,6 +3275,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
|
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns-tz": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decimal.js-light": {
|
"node_modules/decimal.js-light": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
@ -5659,6 +5677,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
|
||||||
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
|
"integrity": "sha512-LBrd7MiJZ9McsOgxqWX7AaxrDjcFVjWH/tIKJd7pnR7McaslGYOP1QmmiBXdJH/H/yLCT+rcQ7FaPBUxRGUtrg=="
|
||||||
},
|
},
|
||||||
|
"@date-fns/utc": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/@date-fns/utc/-/utc-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA=="
|
||||||
|
},
|
||||||
"@dnd-kit/accessibility": {
|
"@dnd-kit/accessibility": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
@ -7508,7 +7531,7 @@
|
|||||||
},
|
},
|
||||||
"date-fns": {
|
"date-fns": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
||||||
},
|
},
|
||||||
"date-fns-jalali": {
|
"date-fns-jalali": {
|
||||||
@ -7516,6 +7539,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
|
||||||
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
|
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="
|
||||||
},
|
},
|
||||||
|
"date-fns-tz": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"decimal.js-light": {
|
"decimal.js-light": {
|
||||||
"version": "2.5.1",
|
"version": "2.5.1",
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
"@21st-extension/react": "^0.5.14",
|
"@21st-extension/react": "^0.5.14",
|
||||||
"@21st-extension/toolbar-next": "^0.5.14",
|
"@21st-extension/toolbar-next": "^0.5.14",
|
||||||
"@apollo/client": "^3.13.9",
|
"@apollo/client": "^3.13.9",
|
||||||
|
"@date-fns/utc": "^2.1.1",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@ -38,6 +39,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
"dnd-kit": "^0.0.2",
|
"dnd-kit": "^0.0.2",
|
||||||
"framer-motion": "^12.23.6",
|
"framer-motion": "^12.23.6",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user