Compare commits

..

No commits in common. "9646e5d0e1eb2eb5b34761a3fdaf3ff0ab6345da" and "aafc5078fabdeceda792da0fb34d22a3933ce3ee" have entirely different histories.

13 changed files with 151 additions and 331 deletions

View File

@ -1,14 +1,15 @@
'use client'
import React, { createContext, useContext, useRef, useState, ReactNode } from 'react'
// import Map from 'ol/Map';
import { Map } from 'maplibre-gl';
import { fromLonLat } from 'ol/proj';
// 定义MapContext的类型
interface MapContextType {
mapRef: React.RefObject<Map | null>
layers: React.RefObject<any[]>
mapState: MapState
currentDatetime: Date | null
setMap: (map: Map, layers: any[]) => void
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
zoomIn: () => void
@ -20,8 +21,6 @@ interface MapContextType {
isMapReady: boolean
}
// 创建Context
const MapContext = createContext<MapContextType | undefined>(undefined)
@ -43,7 +42,7 @@ export function MapProvider({ children }: MapProviderProps) {
});
const layersRef = useRef<any[]>([])
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
const setMap = (map: Map, layers: any[]) => {
// 如果已经有地图实例,先清理旧的
@ -52,6 +51,9 @@ export function MapProvider({ children }: MapProviderProps) {
mapRef.current = null;
}
// 监听视图变化事件
// const view = map.getView();
// 监听视图的缩放变化
map.on('zoom', () => {
setMapState(prevState => ({
@ -76,6 +78,12 @@ export function MapProvider({ children }: MapProviderProps) {
const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => {
if (mapRef.current) {
// mapRef.current.getView().animate({
// center: fromLonLat(options.center),
// zoom: options.zoom,
// duration: options.duration || 1000
// })
mapRef.current.flyTo({
center: options.center,
zoom: options.zoom,
@ -87,28 +95,42 @@ export function MapProvider({ children }: MapProviderProps) {
const zoomIn = () => {
if (mapRef.current) {
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! + 1)
mapRef.current.zoomIn()
}
}
const zoomOut = () => {
if (mapRef.current) {
// mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! - 1)
mapRef.current.zoomOut()
}
}
const zoomTo = (zoom: number) => {
if (mapRef.current) {
// mapRef.current.getView().setZoom(zoom)
mapRef.current.zoomTo(zoom)
}
}
const setTime = (date: Date) => {
setCurrentDatetime(date)
if (mapRef.current) {
layersRef.current.forEach(layer => {
const source = layer.getSource()
if (source) {
source.updateParams({
'TIME': date.toISOString()
})
}
})
}
}
const reset = () => {
if (mapRef.current) {
// mapRef.current.getView().setCenter([103.851959, 1.290270])
// mapRef.current.getView().setZoom(11)
mapRef.current.flyTo({
center: [103.851959, 1.290270],
zoom: 11,
@ -132,7 +154,6 @@ export function MapProvider({ children }: MapProviderProps) {
const value: MapContextType = {
setTime,
currentDatetime,
mapRef,
layers: layersRef,
mapState,

View File

@ -18,8 +18,20 @@ import { ThemeToggle } from '@/components/theme-toggle';
// import { Timeline } from '@/app/timeline';
import { Timeline } from '@/app/tl';
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() {
@ -27,6 +39,16 @@ export default function Page() {
const now = new Date();
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 { setTime } = useTimeline()
const { imgBitmap, fetchRadarTile } = useRadarTile({})
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
useEffect(() => {
if (data) {
console.log(data.statusUpdates)
}
}, [data])
return (
<SidebarProvider>
@ -40,7 +62,22 @@ export default function Page() {
</div>
</header>
<div className="relative h-full w-full flex flex-col">
<MapComponent />
<MapComponent imgBitmap={imgBitmap} />
{/* <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 />
</div>
</SidebarInset>

View File

@ -133,6 +133,7 @@ function SelectDemo() {
export const Timeline: React.FC<Props> = ({
startDate,
endDate,
currentDate,
onDateChange,
onPlay,
onPause,
@ -147,12 +148,19 @@ export const Timeline: React.FC<Props> = ({
const canvasRef = 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>({
isDragging: false,
isLongPress: false,
isPanningTimeline: false,
customLineTimestamp: currentDate?.getTime() ?? null,
customLineTimestamp: currentDate?.getTime() ?? new Date().getTime(),
panOffset: 0,
zoomLevel: initialZoom,
});
@ -180,7 +188,6 @@ export const Timeline: React.FC<Props> = ({
const newDate = new Date(state.customLineTimestamp! + 360000);
setState({ ...state, customLineTimestamp: newDate.getTime() });
onDateChange?.(newDate);
setTime(newDate);
}, [state.customLineTimestamp])
// 缩放处理函数
@ -275,7 +282,7 @@ export const Timeline: React.FC<Props> = ({
dpr,
overrides.startDate ?? currentProps.startDate,
overrides.endDate ?? currentProps.endDate,
overrides.currentDate ?? currentProps.currentDate ?? undefined,
overrides.currentDate ?? currentProps.currentDate,
overrides.zoomLevel ?? currentState.zoomLevel,
overrides.panOffset ?? currentState.panOffset,
overrides.customLineTimestamp ?? currentState.customLineTimestamp,
@ -379,24 +386,17 @@ export const Timeline: React.FC<Props> = ({
const selectedTimestamp = visibleStartTime + progress * visibleTimeRange;
// 规整到最近的6分钟整数时间
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
const roundedTimestamp = Math.round(selectedTimestamp / sixMinutesInMs) * sixMinutesInMs;
setState(prevState => ({
...prevState,
customLineTimestamp: roundedTimestamp,
customLineTimestamp: selectedTimestamp,
isLongPress: false,
isDragging: false
}));
// 通知父组件
if (onDateChange) {
onDateChange(new Date(roundedTimestamp));
onDateChange(new Date(selectedTimestamp));
}
// 使用setTime更新时间轴状态
setTime(new Date(roundedTimestamp));
}
}
}
@ -601,19 +601,12 @@ export const Timeline: React.FC<Props> = ({
const finalTimestamp = visibleStartTime + progress * visibleTimeRange;
// 规整到最近的6分钟整数时间
const sixMinutesInMs = 6 * 60 * 1000; // 6分钟 = 360000毫秒
const roundedFinalTimestamp = Math.round(finalTimestamp / sixMinutesInMs) * sixMinutesInMs;
setState(prevState => ({
...prevState,
customLineTimestamp: roundedFinalTimestamp,
customLineTimestamp: finalTimestamp,
isDragging: false,
isLongPress: false
}));
// 使用setTime更新时间轴状态
setTime(new Date(roundedFinalTimestamp));
} else {
setState(prevState => ({ ...prevState, isDragging: false, isLongPress: false }));
}
@ -713,6 +706,9 @@ export const Timeline: React.FC<Props> = ({
current_uniforms.current.currentTimestamp = currentDate.getTime();
}
// 鼠标滚轮缩放和平移
const handleWheel = (e: WheelEvent) => {
e.preventDefault();

View File

@ -3,26 +3,12 @@ import vsSource from './glsl/timeline/vert.glsl';
import fsSource from './glsl/timeline/frag.glsl';
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, UnlockIcon } from "lucide-react";
import { formatInTimeZone } from "date-fns-tz";
import { parse } from "date-fns"
import { ChevronLeft, ChevronRight, HomeIcon, Pause, Play } from "lucide-react";
import { useTimeline } from "@/hooks/use-timeline";
import { Timeline as TimelineEngine, ZoomMode, TimelineConfig } from "@/lib/timeline";
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 {
startTimestamp: number; // Unix 时间戳开始
@ -79,8 +65,7 @@ export const Timeline: React.FC<Props> = React.memo(({
timelineConfig,
...props
}) => {
const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
const [lock, setLock] = useState(false)
const { isPlaying, togglePlay } = useTimeline({})
const canvasRef = useRef<HTMLCanvasElement>(null);
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
const timelineEngineRef = useRef<TimelineEngine | null>(null);
@ -92,21 +77,6 @@ export const Timeline: React.FC<Props> = React.memo(({
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(() => {
let intervalId: NodeJS.Timeout | null = null;
@ -115,6 +85,7 @@ export const Timeline: React.FC<Props> = React.memo(({
intervalId = setInterval(() => {
// 执行时间前进操作
if (timelineEngineRef.current) {
// timelineEngineRef.current.forwardTimeMark(timeStep);
timelineEngineRef.current.playAndEnsureMarkInView(timeStep)
}
}, 1000); // 每秒执行一次,你可以根据需要调整这个间隔
@ -127,12 +98,6 @@ export const Timeline: React.FC<Props> = React.memo(({
};
}, [isPlaying, timeStep]);
useEffect(() => {
if (currentDatetime && !lock) {
timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime())
}
}, [currentDatetime, lock])
useEffect(() => {
if (!ticksCanvasRef.current) return;
@ -167,20 +132,6 @@ export const Timeline: React.FC<Props> = React.memo(({
primaryFontSize: 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
};
@ -259,10 +210,7 @@ export const Timeline: React.FC<Props> = React.memo(({
variant="secondary"
size="icon"
className="size-5"
onClick={() => {
togglePlay()
setLock(true)
}}
onClick={() => togglePlay()}
title={isPlaying ? "暂停" : "播放"}
>
{isPlaying ? <Pause size={10} /> : <Play size={10} />}
@ -312,16 +260,6 @@ export const Timeline: React.FC<Props> = React.memo(({
</select>
</div>
<Button
variant="secondary"
size="icon"
className="size-5"
onClick={() => setLock(!lock)}
title="锁定时间"
>
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
</Button>
</div>

View File

@ -7,8 +7,6 @@ import { useMapLocation } from '@/hooks/use-map-location'
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
import { createColorMap, ColorMapType, } from '@/lib/color-maps'
import { Colorbar } from './colorbar'
import { useRadarTile } from '@/hooks/use-radartile'
import { format, formatInTimeZone } from 'date-fns-tz'
interface MapComponentProps {
style?: string
@ -26,14 +24,13 @@ export function MapComponent({
// center = [103.851959, 1.290270],
// zoom = 11
imgBitmap: propImgBitmap,
colorMapType = 'meteorological',
colorMapType = 'heatmap',
onColorMapChange
}: MapComponentProps) {
const { fetchRadarTile, imgBitmap } = useRadarTile();
const mapContainer = useRef<HTMLDivElement>(null)
const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
const { setMap } = useMap()
const { location } = useMapLocation()
const imgBitmap = propImgBitmap
const texRef = useRef<WebGLTexture | null>(null)
const lutTexRef = useRef<WebGLTexture | null>(null)
const glRef = useRef<WebGL2RenderingContext | null>(null)
@ -41,13 +38,6 @@ export function MapComponent({
const [isReady, setIsReady] = useState<boolean>(false)
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(() => {
if (!mapContainer.current) return
@ -111,9 +101,8 @@ export function MapComponent({
// 对于灰度图RGB通道通常相同取红色通道作为灰度值
float value = texColor.r * 3.4;
if (value < 0.07) {
fragColor= vec4(1.0,1.0,1.0,0.2);
return;
if (value == 0.0) {
discard;
}
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
@ -122,9 +111,8 @@ export function MapComponent({
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
// 添加一些透明度,使低值区域更透明
// float alpha = smoothstep(0.0, 0.1, value);
float alpha = 0.7;
float alpha = 1.0;
fragColor = vec4(lutColor.rgb, alpha);
// fragColor = vec4(1.0,1.0,1.0,0.2);
}`
console.log(vertexSource, fragmentSource)
@ -178,8 +166,8 @@ export function MapComponent({
}
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
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);
@ -475,10 +463,6 @@ export function MapComponent({
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
// Redraw the map
mapRef.current?.triggerRepaint()
}
}, [imgBitmap, isReady])
@ -572,8 +556,8 @@ function createLutTexture(gl: WebGL2RenderingContext, colorMapType: ColorMapType
lut
)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
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);

View File

@ -69,8 +69,7 @@ export function Timeline({
const newDays = Math.round((newProgress / 100) * totalDays)
const newDate = addDays(startDate, newDays)
onDateChange?.(newDate)
timeline.setTime(newDate)
}, [startDate, totalDays, onDateChange, timeline])
}, [startDate, totalDays, onDateChange])
const handlePlayPause = useCallback(() => {
if (isPlaying) {
@ -83,14 +82,12 @@ export function Timeline({
const handleSkipBack = useCallback(() => {
const newDate = subDays(currentDate, 1)
onDateChange?.(newDate)
timeline.setTime(newDate)
}, [currentDate, onDateChange, timeline])
}, [currentDate, onDateChange])
const handleSkipForward = useCallback(() => {
const newDate = addDays(currentDate, 1)
onDateChange?.(newDate)
timeline.setTime(newDate)
}, [currentDate, onDateChange, timeline])
}, [currentDate, onDateChange])
const speedOptions = [
{ value: 'slow', label: '慢速', interval: 2000 },

View File

@ -24,7 +24,7 @@ const LOCATIONS = {
export type LocationKey = keyof typeof LOCATIONS
export function useMapLocation() {
const [currentLocation, setCurrentLocation] = useState<LocationKey>('china')
const [currentLocation, setCurrentLocation] = useState<LocationKey>('usa')
const { flyTo, isMapReady } = useMap()
const flyToLocation = useCallback((location: LocationKey) => {

View File

@ -1,19 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { addDays, subDays } from 'date-fns'
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';
const speedIntervals = {
slow: 2000,
normal: 1000,
fast: 500
}
interface UseTimelineOptions {
startDate?: Date
@ -21,27 +8,33 @@ interface UseTimelineOptions {
initialDate?: Date
onDateChange?: (date: Date) => void
autoPlay?: boolean
autoUpdate?: 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, currentDatetime } = useMap()
const { setTime } = useMap()
const speedIntervals = {
slow: 2000,
normal: 1000,
fast: 500
}
const updateDate = useCallback((newDate: Date) => {
setTime(newDate)
setCurrentDate(newDate)
onDateChange?.(newDate)
}, [onDateChange])
const play = useCallback(() => {
setIsPlaying(true)
}, [])
@ -55,20 +48,18 @@ export function useTimeline({
}, [])
const skipForward = useCallback(() => {
if (!currentDatetime) return;
const newDate = addDays(currentDatetime, 1)
const newDate = addDays(currentDate, 1)
if (newDate <= endDate) {
updateDate(newDate)
}
}, [currentDatetime, endDate, updateDate])
}, [currentDate, endDate, updateDate])
const skipBackward = useCallback(() => {
if (!currentDatetime) return;
const newDate = subDays(currentDatetime, 1)
const newDate = subDays(currentDate, 1)
if (newDate >= startDate) {
updateDate(newDate)
}
}, [currentDatetime, startDate, updateDate])
}, [currentDate, startDate, updateDate])
const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => {
setSpeed(newSpeed)
@ -84,8 +75,7 @@ export function useTimeline({
useEffect(() => {
if (isPlaying) {
intervalRef.current = setInterval(() => {
if (!currentDatetime) return;
const nextDate = addDays(currentDatetime, 1)
const nextDate = addDays(currentDate, 1)
if (nextDate <= endDate) {
updateDate(nextDate)
} else {
@ -105,7 +95,7 @@ export function useTimeline({
clearInterval(intervalRef.current)
}
}
}, [isPlaying, currentDatetime, endDate, speed, updateDate])
}, [isPlaying, currentDate, endDate, speed, updateDate])
// 清理定时器
useEffect(() => {
@ -117,7 +107,7 @@ export function useTimeline({
}, [])
return {
currentDatetime,
currentDate,
isPlaying,
speed,
startDate,

View File

@ -1,5 +1,5 @@
// 色标函数集合
export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale' | 'meteorological';
export type ColorMapType = 'radar' | 'rainbow' | 'heatmap' | 'viridis' | 'plasma' | 'grayscale';
// 雷达色标 (深蓝到红色,类似气象雷达)
export function createRadarColorMap(): Uint8Array {
@ -207,109 +207,11 @@ export function createColorMap(type: ColorMapType): Uint8Array {
return createPlasmaColorMap();
case 'grayscale':
return createGrayscaleColorMap();
case 'meteorological':
return createMeteorologicalColorMap();
default:
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 }[] {
return [
@ -318,7 +220,6 @@ export function getAvailableColorMaps(): { value: ColorMapType; label: string }[
{ value: 'heatmap', label: '热力图色标' },
{ value: 'viridis', label: 'Viridis色标' },
{ value: 'plasma', label: 'Plasma色标' },
{ value: 'grayscale', label: '灰度色标' },
{ value: 'meteorological', label: '气象雷达色标' }
{ value: 'grayscale', label: '灰度色标' }
];
}

View File

@ -3,8 +3,6 @@
* globe模式下的球面渲染
*/
import { MercatorCoordinate } from 'maplibre-gl'
export interface TileMeshOptions {
/** 经纬度边界 [west, south, east, north] */
bounds: [number, number, number, number];
@ -165,12 +163,10 @@ export function detectPerformanceLevel(): PerformanceLevel {
* Web Mercator坐标 (0-1)
*/
function lonLatToMercator(lon: number, lat: number): [number, number] {
const mercator = MercatorCoordinate.fromLngLat({ lng: lon, lat: lat })
return [mercator.x, mercator.y]
// const x = (lon + 180) / 360;
// const latRad = (lat * Math.PI) / 180;
// const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
// return [x, y];
const x = (lon + 180) / 360;
const latRad = (lat * Math.PI) / 180;
const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
return [x, y];
}
/**

View File

@ -110,8 +110,6 @@ interface TimelineConfig {
highlightWeekends?: boolean;
/** 时间标记列表 */
timeMarks?: TimeMark[];
onDateChange?: (date: Date) => void;
}
/** 时间工具类 */
@ -651,8 +649,7 @@ class RealTimeTimeline {
},
showCurrentTime: true,
highlightWeekends: true,
timeMarks: [],
onDateChange: () => { }
timeMarks: []
};
if (!config) return defaultConfig;
@ -667,8 +664,7 @@ class RealTimeTimeline {
sizes: { ...defaultConfig.sizes, ...config.sizes },
showCurrentTime: config.showCurrentTime ?? defaultConfig.showCurrentTime,
highlightWeekends: config.highlightWeekends ?? defaultConfig.highlightWeekends,
timeMarks: config.timeMarks ?? defaultConfig.timeMarks,
onDateChange: config.onDateChange ?? defaultConfig.onDateChange
timeMarks: config.timeMarks ?? defaultConfig.timeMarks
};
}
@ -727,7 +723,13 @@ class RealTimeTimeline {
// 获取当前刻度信息用于吸附
const ticks = this.scaleManager.calculateTicks(this.viewport);
const date = this.viewport.screenToTime(x, true, ticks);
this.changeTime(new Date(date))
this.interaction.setZoomMode(ZoomMode.MarkMode);
this.replaceTimeMark({
timestamp: date,
color: '#ff6b6b',
label: format(date, 'HH:mm:ss'),
type: 'custom'
});
}
});
@ -812,17 +814,6 @@ class RealTimeTimeline {
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 {
const [startTime, endTime] = this.viewport.getVisibleRange();
@ -1047,25 +1038,19 @@ class RealTimeTimeline {
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 {
if (!timestamp) {
timestamp = this.config.timeMarks[0].timestamp;
}
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
if (index !== -1) {
this.changeTime(new Date(timestamp + delta))
this.config.timeMarks.splice(index, 1);
this.replaceTimeMark({
timestamp: timestamp + delta,
color: '#ff6b6b',
label: format(timestamp + delta, 'HH:mm:ss'),
type: 'custom'
});
this.render();
}
}
@ -1076,7 +1061,13 @@ class RealTimeTimeline {
}
const index = this.config.timeMarks.findIndex(mark => mark.timestamp === timestamp);
if (index !== -1) {
this.changeTime(new Date(timestamp - delta))
this.config.timeMarks.splice(index, 1);
this.replaceTimeMark({
timestamp: timestamp - delta,
color: '#ff6b6b',
label: format(timestamp - delta, 'HH:mm:ss'),
type: 'custom'
});
this.render();
}
}

33
package-lock.json generated
View File

@ -11,7 +11,6 @@
"@21st-extension/react": "^0.5.14",
"@21st-extension/toolbar-next": "^0.5.14",
"@apollo/client": "^3.13.9",
"@date-fns/utc": "^2.1.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@ -38,7 +37,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dnd-kit": "^0.0.2",
"framer-motion": "^12.23.6",
"graphql": "^16.11.0",
@ -198,12 +196,6 @@
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"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": {
"version": "3.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -3262,9 +3254,8 @@
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
@ -3275,15 +3266,6 @@
"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=="
},
"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": {
"version": "2.5.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@ -5677,11 +5659,6 @@
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.2.0.tgz",
"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": {
"version": "3.1.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
@ -7531,7 +7508,7 @@
},
"date-fns": {
"version": "4.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/date-fns/-/date-fns-4.1.0.tgz",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
},
"date-fns-jalali": {
@ -7539,12 +7516,6 @@
"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=="
},
"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": {
"version": "2.5.1",
"resolved": "http://mirrors.cloud.tencent.com/npm/decimal.js-light/-/decimal.js-light-2.5.1.tgz",

View File

@ -12,7 +12,6 @@
"@21st-extension/react": "^0.5.14",
"@21st-extension/toolbar-next": "^0.5.14",
"@apollo/client": "^3.13.9",
"@date-fns/utc": "^2.1.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
@ -39,7 +38,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"dnd-kit": "^0.0.2",
"framer-motion": "^12.23.6",
"graphql": "^16.11.0",