This commit is contained in:
tsuki 2025-08-13 21:54:20 +08:00
parent 70c790c93d
commit 12fad1711c
11 changed files with 516 additions and 153 deletions

46
app/glsl/radar/frag.glsl Normal file
View File

@ -0,0 +1,46 @@
#version 300 es
precision highp float;
uniform sampler2D u_tex;
uniform sampler2D u_lut;
in vec2 lonlat;
out vec4 fragColor;
const float PI = 3.141592653589793;
const vec4 bound = vec4(65.24686922, 13.12169419,
138.85867996, 55.34323806);
void main() {
vec2 uv = vec2(
(lonlat.x - bound.x) / (bound.z - bound.x), // 经度映射到 u
1.0 - (lonlat.y - bound.y) / (bound.w - bound.y) // 纬度映射到 v
);
// 调试:可以取消注释下面的行来可视化坐标
// fragColor = vec4(uv.x, 0.0, 0.0, 1.0); return; // UV坐标可视化
// 显示原始经纬度值归一化到0-1范围用于可视化
// fragColor = vec4((lonlat.x - 60.0)/100.0, (lonlat.y + 10.0)/70.0, 0.0, 1.0);
// return; // 经纬度可视化
if(any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) {
discard;
}
vec4 texColor = texture(u_tex, uv);
if (texColor.r <= 0.0196 || texColor.r > 0.29411) {
discard;
}
float value = texColor.r * 3.4;
value = clamp(value, 0.0, 1.0);
float alpha = smoothstep(0.07, 0.12, value) * 0.9;
if (alpha <= 0.001) discard;
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
fragColor = vec4(lutColor.rgb, alpha);
}

128
app/glsl/radar/verx.glsl Normal file
View File

@ -0,0 +1,128 @@
#version 300 es
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_tex_coord;
const float PI = 3.141592653589793;
uniform mat4 u_projection_matrix;
#define GLOBE_RADIUS 6371008.8
uniform highp vec4 u_projection_tile_mercator_coords;
uniform highp vec4 u_projection_clipping_plane;
uniform highp float u_projection_transition;
uniform mat4 u_projection_fallback_matrix;
vec3 globeRotateVector(vec3 vec,vec2 angles) {
vec3 axisRight=vec3(vec.z,0.0,-vec.x);
vec3 axisUp=cross(axisRight,vec);
axisRight=normalize(axisRight);
axisUp=normalize(axisUp);
vec2 t=tan(angles);
return normalize(vec+axisRight*t.x+axisUp*t.y);
}
mat3 globeGetRotationMatrix(vec3 spherePos) {
vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);
vec3 axisDown=cross(axisRight,spherePos);
axisRight=normalize(axisRight);
axisDown=normalize(axisDown);
return mat3(axisRight,axisDown,spherePos);
}
float circumferenceRatioAtTileY(float tileY) {
float mercator_pos_y = u_projection_tile_mercator_coords.y + u_projection_tile_mercator_coords.w*tileY;
float spherical_y = 2.0*atan(exp(PI-(mercator_pos_y*PI*2.0))) - PI*0.5;return cos(spherical_y);
}
float projectLineThickness(float tileY) {
float thickness=1.0/circumferenceRatioAtTileY(tileY);
if (u_projection_transition < 0.999) {
return mix(1.0,thickness,u_projection_transition);
} else {
return thickness;
}
}
vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {
vec2 mercator_pos = u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;
vec2 spherical;
spherical.x=mercator_pos.x*PI*2.0+PI;
spherical.y=2.0*atan(exp(PI-(mercator_pos.y*PI*2.0)))-PI*0.5;
float len=cos(spherical.y);
vec3 pos=vec3(sin(spherical.x)*len,sin(spherical.y),cos(spherical.x)*len);
if (rawPos.y <-32767.5) {
pos = vec3(0.0,1.0,0.0);
}
if (rawPos.y > 32766.5) {
pos=vec3(0.0,-1.0,0.0);
}
return pos;
}
vec3 projectToSphere(vec2 posInTile) {
return projectToSphere(posInTile,vec2(0.0,0.0));
}
vec2 computeLatLon(vec2 posInTile) {
vec2 mercator_pos = u_projection_tile_mercator_coords.xy + u_projection_tile_mercator_coords.zw * posInTile;
vec2 spherical;
spherical.x = mercator_pos.x * PI * 2.0 + PI; // 经度(弧度)
spherical.y = 2.0 * atan(exp(PI - (mercator_pos.y * PI * 2.0))) - PI * 0.5; // 纬度(弧度)
vec2 lonlat_degrees;
lonlat_degrees.x = spherical.x * 180.0 / PI; // 经度(角度)
lonlat_degrees.y = spherical.y * 180.0 / PI; // 纬度(角度)
// 确保经度在0-360范围内
lonlat_degrees.x = mod(lonlat_degrees.x + 360.0, 360.0);
return lonlat_degrees;
}
float globeComputeClippingZ(vec3 spherePos) {
return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));
}
vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {
vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);
vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);
globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;
if (u_projection_transition > 0.999) {return globePosition;}
vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);
const float z_globeness_threshold=0.2;
vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);
if (u_projection_transition > 0.999) {return globePosition;}
vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);
return mix(fallbackPosition,globePosition,u_projection_transition);
}
vec4 projectTile(vec2 posInTile) {
return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);
}
vec4 projectTile(vec2 posInTile,vec2 rawPos) {
return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);
}
vec4 projectTileWithElevation(vec2 posInTile,float elevation) {
return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);
}
vec4 projectTileFor3D(vec2 posInTile,float elevation) {
vec3 spherePos=projectToSphere(posInTile,posInTile);
return interpolateProjectionFor3D(posInTile,spherePos,elevation);
}
#define GLOBE
out vec2 lonlat;
void main() {
gl_Position = projectTile(a_pos);
lonlat = computeLatLon(a_pos);
}

View File

@ -22,6 +22,8 @@ import { useMap } from './map-context'
import { format } from 'date-fns'
import { Label } from '@/components/ui/label'
import { Navigation } from './nav'
import { WSProvider } from './ws-context'
import StatusBar from './status-bar'
@ -30,7 +32,9 @@ export default function Page() {
return (
<div className="flex flex-row h-full">
<AppSidebar />
<WSProvider>
<div className="flex-1 flex flex-col min-h-0">
<StatusBar />
<div className="flex-1 min-h-0">
<MapComponent />
</div>
@ -38,6 +42,8 @@ export default function Page() {
<Timeline />
</div>
</div>
</WSProvider>
</div>
)
}

33
app/status-bar.tsx Normal file
View File

@ -0,0 +1,33 @@
"use client"
import { Label } from "@/components/ui/label"
import { useWS } from "./ws-context"
export default function StatusBar() {
const { wsStatus } = useWS()
// 根据WebSocket状态返回对应颜色的圆点
const getStatusDot = () => {
switch (wsStatus) {
case 'connected':
return <div className="w-2 h-2 bg-green-500 rounded-full" title="已连接" />
case 'connecting':
return <div className="w-2 h-2 bg-yellow-500 rounded-full animate-pulse" title="连接中..." />
case 'disconnected':
default:
return <div className="w-2 h-2 bg-red-500 rounded-full" title="连接断开" />
}
}
return (
<div className="h-8 flex items-center justify-end px-4">
<div className="flex items-center">
{getStatusDot()}
<Label className="ml-2 text-xs font-bold text-muted-foreground">
{wsStatus}
</Label>
</div>
</div>
)
}

View File

@ -10,19 +10,8 @@ import { parse } from "date-fns"
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";
import { useWS } from "./ws-context";
const SUBSCRIPTION_QUERY = gql`
subscription {
statusUpdates {
id
message
status
timestamp
newestDt
}
}
`
interface Uniforms {
startTimestamp: number; // Unix 时间戳开始
@ -79,7 +68,7 @@ export const Timeline: React.FC<Props> = React.memo(({
timelineConfig,
...props
}) => {
const { isPlaying, togglePlay, currentDatetime, setTime, setTimelineTime, timelineDatetime } = useTimeline({})
const { isPlaying, togglePlay, setTime, setTimelineTime, setWsStatus } = useTimeline({})
const [lock, setLock] = useState(false)
const canvasRef = useRef<HTMLCanvasElement>(null);
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
@ -92,8 +81,7 @@ export const Timeline: React.FC<Props> = React.memo(({
currentLevel: null as any
});
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
const { data } = useWS()
useEffect(() => {
if (data) {

85
app/ws-context.tsx Normal file
View File

@ -0,0 +1,85 @@
'use client'
import React, { createContext, useContext, useRef, useState, ReactNode, useEffect } from 'react'
import { Map } from 'maplibre-gl';
import { gql, useSubscription } from '@apollo/client';
// 定义MapContext的类型
interface WSContextType {
wsStatus: WsStatus
setWsStatus: (status: WsStatus) => void
data: any
loading: boolean
error: any
restart: () => void
}
enum WsStatus {
CONNECTING = 'connecting',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected'
}
// 创建Context
const WSContext = createContext<WSContextType | undefined>(undefined)
// Provider组件的Props类型
interface MapProviderProps {
children: ReactNode
}
const SUBSCRIPTION_QUERY = gql`
subscription {
statusUpdates {
id
message
status
timestamp
newestDt
}
}
`
// Provider组件
export function WSProvider({ children }: MapProviderProps) {
const [wsStatus, setWsStatus] = useState<WsStatus>(WsStatus.DISCONNECTED)
const { data, loading, error, restart } = useSubscription(SUBSCRIPTION_QUERY)
useEffect(() => {
if (loading) {
setWsStatus(WsStatus.CONNECTING)
} else if (error) {
setWsStatus(WsStatus.DISCONNECTED)
} else {
setWsStatus(WsStatus.CONNECTED)
}
}, [data])
const value: WSContextType = {
wsStatus,
setWsStatus,
data,
loading,
error,
restart
}
return (
<WSContext.Provider value={value}>
{children}
</WSContext.Provider>
)
}
// 自定义Hook用于使用MapContext
export function useWS() {
const context = useContext(WSContext)
if (context === undefined) {
throw new Error('useWS must be used within a WSProvider')
}
return context
}

View File

@ -11,6 +11,7 @@ interface ColorbarProps {
minValue?: number
maxValue?: number
unit?: string
orientation?: 'horizontal' | 'vertical'
}
export function Colorbar({
@ -20,7 +21,8 @@ export function Colorbar({
showLabels = true,
minValue = 0,
maxValue = 100,
unit = ''
unit = '',
orientation = 'horizontal'
}: ColorbarProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [isHovered, setIsHovered] = useState(false)
@ -43,6 +45,8 @@ export function Colorbar({
// 绘制色标
const imageData = ctx.createImageData(width, height)
if (orientation === 'horizontal') {
// 水平方向:从左到右
for (let x = 0; x < width; x++) {
const t = x / (width - 1) // 归一化到 0-1
const colorIndex = Math.floor(t * 255) * 4
@ -60,6 +64,26 @@ export function Colorbar({
imageData.data[pixelIndex + 3] = a
}
}
} else {
// 垂直方向:从下到上
for (let y = 0; y < height; y++) {
const t = (height - 1 - y) / (height - 1) // 归一化到 0-1颠倒方向
const colorIndex = Math.floor(t * 255) * 4
const r = colorMap[colorIndex]
const g = colorMap[colorIndex + 1]
const b = colorMap[colorIndex + 2]
const a = colorMap[colorIndex + 3]
for (let x = 0; x < width; x++) {
const pixelIndex = (y * width + x) * 4
imageData.data[pixelIndex] = r
imageData.data[pixelIndex + 1] = g
imageData.data[pixelIndex + 2] = b
imageData.data[pixelIndex + 3] = a
}
}
}
ctx.putImageData(imageData, 0, 0)
@ -68,15 +92,23 @@ export function Colorbar({
ctx.lineWidth = 1
ctx.strokeRect(0, 0, width, height)
}, [colorMapType, width, height])
}, [colorMapType, width, height, orientation])
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
let t: number
if (orientation === 'horizontal') {
const x = e.clientX - rect.left
const t = x / width
t = x / width
} else {
const y = e.clientY - rect.top
t = (height - y) / height // 垂直方向:从下到上
}
const value = minValue + t * (maxValue - minValue)
setHoverValue(value)
setIsHovered(true)
@ -100,7 +132,7 @@ export function Colorbar({
}
return (
<div className="relative">
<div className={orientation === 'horizontal' ? 'relative' : 'flex items-center'}>
<canvas
ref={canvasRef}
className="cursor-crosshair"
@ -110,20 +142,33 @@ export function Colorbar({
/>
{showLabels && (
orientation === 'horizontal' ? (
<div className="flex justify-between text-xs text-gray-600 mt-1">
<span>{formatValue(minValue)}</span>
<span>{formatValue(maxValue)}</span>
</div>
) : (
<div className="flex flex-col justify-between text-xs text-gray-600 ml-2 h-full" style={{ height: `${height}px` }}>
<span>{formatValue(maxValue)}</span>
<span>{formatValue(minValue)}</span>
</div>
)
)}
{isHovered && hoverValue !== null && (
<div
className="absolute bg-black text-white text-xs px-2 py-1 rounded pointer-events-none z-10"
style={{
className="absolute bg-black text-white text-xs px-2 py-1 pointer-events-none z-10"
style={
orientation === 'horizontal' ? {
left: `${(hoverValue - minValue) / (maxValue - minValue) * width}px`,
top: '-30px',
transform: 'translateX(-50%)'
}}
} : {
left: `${width + 10}px`,
top: `${((maxValue - hoverValue) / (maxValue - minValue)) * height}px`,
transform: 'translateY(-50%)'
}
}
>
{formatValue(hoverValue)}
</div>

View File

@ -1,6 +1,6 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, } from 'maplibre-gl'
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, LogoControl, } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { useMap } from '@/app/map-context'
import { useMapLocation } from '@/hooks/use-map-location'
@ -9,6 +9,8 @@ import { createColorMap, ColorMapType, } from '@/lib/color-maps'
import { Colorbar } from './colorbar'
import { useRadarTile } from '@/hooks/use-radartile'
import { format, formatInTimeZone } from 'date-fns-tz'
import vertexSource from '@/app/glsl/radar/verx.glsl'
import fragmentSource from '@/app/glsl/radar/frag.glsl'
interface MapComponentProps {
style?: string
@ -41,6 +43,18 @@ export function MapComponent({
const [isReady, setIsReady] = useState<boolean>(false)
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
// 拖动状态
const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 16 }) // 从右边和下边的距离
const [isDragging, setIsDragging] = useState(false)
// 使用ref来避免频繁的状态更新
const dragRef = useRef({
startX: 0,
startY: 0,
startPositionX: 0,
startPositionY: 0
})
useEffect(() => {
if (!isMapReady || !currentDatetime) return;
const utc_time_str = formatInTimeZone(currentDatetime, 'UTC', 'yyyyMMddHHmmss')
@ -56,12 +70,15 @@ export function MapComponent({
style: style,
center: location.center,
zoom: location.zoom,
attributionControl: false, // 禁用默认的版权控制
canvasContextAttributes: {
contextType: 'webgl2', // 请求 WebGL2
antialias: true // 打开多重采样抗锯齿
}
},
})
map.on('style.load', () => {
map.setProjection({
type: 'globe'
@ -82,85 +99,6 @@ export function MapComponent({
return;
}
const vertexSource = `#version 300 es
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_tex_coord;
${shaderData.vertexShaderPrelude}
${shaderData.define}
out vec2 v_merc; // 归一化墨卡托坐标 (0..1)
void main() {
gl_Position = projectTile(a_pos);
// 用 MapLibre 注入的 u_projection_tile_mercator_coords 把 tile 坐标变为 mercator 归一坐标
// merc = offset.xy + scale.zw * a_pos
v_merc = u_projection_tile_mercator_coords.xy +
u_projection_tile_mercator_coords.zw * a_pos;
}
`
// WebGL2 fragment shader
const fragmentSource = `#version 300 es
precision highp float;
uniform sampler2D u_tex;
uniform sampler2D u_lut;
in vec2 v_merc; // 来自 VS 的归一化墨卡托
out vec4 fragColor;
const float PI = 3.141592653589793;
// 假设 bound = vec4(west, south, east, north) in degrees
const vec4 bound = vec4(65.24686921730095, 11.90274236858339,
138.85323419021077, 55.34323805611308);
// mercator -> (lon, lat) in radians
vec2 mercatorToLonLat(vec2 merc) {
float lon = merc.x * 2.0 * PI - PI; // 修正这里:-PI
float lat = 2.0 * atan(exp(PI - merc.y * 2.0 * PI)) - 0.5 * PI;
return vec2(lon, lat);
}
vec2 rad2deg(vec2 r) { return r * (180.0 / PI); }
void main() {
// 归一化墨卡托 -> 经纬度(度)
vec2 lonlat = rad2deg(mercatorToLonLat(v_merc));
// 经纬度 -> 纹理UV按 bound 线性映射)
vec2 uv = vec2(
(lonlat.x - bound.x) / (bound.z - bound.x),
1.0-(lonlat.y - bound.y) / (bound.w - bound.y)
);
// 如果超出范围直接丢弃,避免采到边界垃圾
if(any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) {
discard;
}
vec4 texColor = texture(u_tex, uv);
if (texColor.r <= 0.0196 || texColor.r > 0.29411) {
discard;
}
float value = texColor.r * 3.4;
value = clamp(value, 0.0, 1.0);
// 软阈值,避免全场被 return
float alpha = smoothstep(0.07, 0.12, value) * 0.9;
if (alpha <= 0.001) discard;
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
fragColor = vec4(lutColor.rgb, alpha);
}
`;
console.log(vertexSource, fragmentSource)
// Helper function to compile shader
const compileShader = (source: string, type: number): WebGLShader | null => {
const shader = gl.createShader(type);
@ -540,6 +478,73 @@ export function MapComponent({
}
}, [currentColorMapType, isReady])
// 拖动事件处理函数
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
setIsDragging(true)
// 记录拖动开始时的鼠标位置和colorbar位置
dragRef.current = {
startX: e.clientX,
startY: e.clientY,
startPositionX: colorbarPosition.x,
startPositionY: colorbarPosition.y
}
}
// 全局鼠标事件监听
useEffect(() => {
if (!isDragging) return
let animationFrameId: number
const handleGlobalMouseMove = (e: MouseEvent) => {
// 使用requestAnimationFrame来优化性能
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
animationFrameId = requestAnimationFrame(() => {
const mapContainer = document.querySelector('.h-full.w-full.relative') as HTMLElement
if (!mapContainer) return
const containerRect = mapContainer.getBoundingClientRect()
// 计算鼠标移动的总距离
const deltaX = dragRef.current.startX - e.clientX
const deltaY = dragRef.current.startY - e.clientY
// 计算新位置(相对于容器右下角的距离)
const newX = dragRef.current.startPositionX + deltaX
const newY = dragRef.current.startPositionY + deltaY
// 限制在容器范围内
const clampedX = Math.max(0, Math.min(newX, containerRect.width - 50)) // 预留colorbar宽度
const clampedY = Math.max(0, Math.min(newY, containerRect.height - 220)) // 预留colorbar高度
setColorbarPosition({ x: clampedX, y: clampedY })
})
}
const handleGlobalMouseUp = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
setIsDragging(false)
}
document.addEventListener('mousemove', handleGlobalMouseMove)
document.addEventListener('mouseup', handleGlobalMouseUp)
return () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
document.removeEventListener('mousemove', handleGlobalMouseMove)
document.removeEventListener('mouseup', handleGlobalMouseUp)
}
}, [isDragging]) // 只依赖isDragging避免频繁重新创建事件监听器
return (
<div className="h-full w-full relative">
<div
@ -548,16 +553,27 @@ export function MapComponent({
style={{ minHeight: '400px' }}
/>
{/* Colorbar 在右下角 */}
<div className="absolute bottom-4 right-4 bg-white/90 backdrop-blur-sm rounded-lg p-3 shadow-lg border">
<div className="text-xs text-gray-600 mb-2 font-medium"></div>
{/* 可拖动的 Colorbar */}
<div
data-colorbar
className={`absolute select-none ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
style={{
right: `${colorbarPosition.x}px`,
bottom: `${colorbarPosition.y}px`,
transform: isDragging ? 'scale(1.02)' : 'scale(1)',
transition: isDragging ? 'none' : 'transform 0.1s ease-out',
willChange: isDragging ? 'transform' : 'auto'
}}
onMouseDown={handleMouseDown}
>
<Colorbar
colorMapType={currentColorMapType}
width={180}
height={16}
width={16}
height={220}
minValue={0}
maxValue={75}
unit="dBZ"
orientation="vertical"
/>
</div>
</div>

View File

@ -24,6 +24,12 @@ interface UseTimelineOptions {
autoUpdate?: boolean
}
enum WsStatus {
CONNECTING = 'connecting',
CONNECTED = 'connected',
DISCONNECTED = 'disconnected'
}
export function useTimeline({
startDate = subDays(new Date(), 30),
endDate = new Date(),
@ -35,6 +41,7 @@ export function useTimeline({
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const { setTime, currentDatetime, setTimelineTime, timelineDatetime } = useMap()
const [wsStatus, setWsStatus] = useState<WsStatus>(WsStatus.DISCONNECTED)
const updateDate = useCallback((newDate: Date) => {
setTime(newDate)
@ -132,6 +139,8 @@ export function useTimeline({
changeSpeed,
jumpToDate,
updateDate,
setTime
setTime,
wsStatus,
setWsStatus
}
}

View File

@ -266,21 +266,10 @@ export function createMeteorologicalColorMap(): Uint8Array {
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;
}

View File

@ -63,6 +63,24 @@ vec3 projectToSphere(vec2 posInTile) {
return projectToSphere(posInTile,vec2(0.0,0.0));
}
// 计算纬度和经度(以角度为单位)
vec2 computeLatLon(vec2 posInTile) {
vec2 mercator_pos = u_projection_tile_mercator_coords.xy + u_projection_tile_mercator_coords.zw * posInTile;
vec2 spherical;
spherical.x = mercator_pos.x * PI * 2.0 + PI; // 经度(弧度)
spherical.y = 2.0 * atan(exp(PI - (mercator_pos.y * PI * 2.0))) - PI * 0.5; // 纬度(弧度)
// 转换为角度
vec2 lonlat_degrees;
lonlat_degrees.x = spherical.x * 180.0 / PI; // 经度(角度)
lonlat_degrees.y = spherical.y * 180.0 / PI; // 纬度(角度)
// 确保经度在0-360范围内
lonlat_degrees.x = mod(lonlat_degrees.x + 360.0, 360.0);
return lonlat_degrees;
}
float globeComputeClippingZ(vec3 spherePos) {
return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));
}
@ -104,9 +122,9 @@ vec4 projectTileFor3D(vec2 posInTile,float elevation) {
#define GLOBE
out vec2 v_tex_coord;
out vec2 lonlat;
void main() {
gl_Position = projectTile(a_pos);
v_tex_coord = a_tex_coord;
lonlat = computeLatLon(a_pos);
}