From 12fad1711c10a5e13ed438ea0f948ba20247eb44 Mon Sep 17 00:00:00 2001 From: tsuki Date: Wed, 13 Aug 2025 21:54:20 +0800 Subject: [PATCH] refactor --- app/glsl/radar/frag.glsl | 46 +++++++++ app/glsl/radar/verx.glsl | 128 ++++++++++++++++++++++++ app/page.tsx | 20 ++-- app/status-bar.tsx | 33 ++++++ app/tl.tsx | 18 +--- app/ws-context.tsx | 85 ++++++++++++++++ components/colorbar.tsx | 99 +++++++++++++----- components/map-component.tsx | 188 +++++++++++++++++++---------------- hooks/use-timeline.ts | 11 +- lib/color-maps.ts | 19 +--- wind.glsl | 22 +++- 11 files changed, 516 insertions(+), 153 deletions(-) create mode 100644 app/glsl/radar/frag.glsl create mode 100644 app/glsl/radar/verx.glsl create mode 100644 app/status-bar.tsx create mode 100644 app/ws-context.tsx diff --git a/app/glsl/radar/frag.glsl b/app/glsl/radar/frag.glsl new file mode 100644 index 0000000..994d79e --- /dev/null +++ b/app/glsl/radar/frag.glsl @@ -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); +} \ No newline at end of file diff --git a/app/glsl/radar/verx.glsl b/app/glsl/radar/verx.glsl new file mode 100644 index 0000000..953fe01 --- /dev/null +++ b/app/glsl/radar/verx.glsl @@ -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); +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 72444e7..079a3bb 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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,14 +32,18 @@ export default function Page() { return (
-
-
- + +
+ +
+ +
+
+ +
-
- -
-
+ +
) } diff --git a/app/status-bar.tsx b/app/status-bar.tsx new file mode 100644 index 0000000..d1d7a9f --- /dev/null +++ b/app/status-bar.tsx @@ -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
+ case 'connecting': + return
+ case 'disconnected': + default: + return
+ } + } + + return ( +
+
+ {getStatusDot()} + +
+
+ ) +} \ No newline at end of file diff --git a/app/tl.tsx b/app/tl.tsx index 8462cf9..476afcb 100644 --- a/app/tl.tsx +++ b/app/tl.tsx @@ -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 = 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(null); const ticksCanvasRef = useRef(null); @@ -92,8 +81,7 @@ export const Timeline: React.FC = React.memo(({ currentLevel: null as any }); - - const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY) + const { data } = useWS() useEffect(() => { if (data) { diff --git a/app/ws-context.tsx b/app/ws-context.tsx new file mode 100644 index 0000000..89de075 --- /dev/null +++ b/app/ws-context.tsx @@ -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(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.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 ( + + {children} + + ) +} + +// 自定义Hook用于使用MapContext +export function useWS() { + const context = useContext(WSContext) + + if (context === undefined) { + throw new Error('useWS must be used within a WSProvider') + } + + return context +} \ No newline at end of file diff --git a/components/colorbar.tsx b/components/colorbar.tsx index a5448fc..8e6aeef 100644 --- a/components/colorbar.tsx +++ b/components/colorbar.tsx @@ -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(null) const [isHovered, setIsHovered] = useState(false) @@ -43,21 +45,43 @@ export function Colorbar({ // 绘制色标 const imageData = ctx.createImageData(width, height) - for (let x = 0; x < width; x++) { - const t = x / (width - 1) // 归一化到 0-1 - const colorIndex = Math.floor(t * 255) * 4 + if (orientation === 'horizontal') { + // 水平方向:从左到右 + for (let x = 0; x < width; x++) { + const t = x / (width - 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] + const r = colorMap[colorIndex] + const g = colorMap[colorIndex + 1] + const b = colorMap[colorIndex + 2] + const a = colorMap[colorIndex + 3] + for (let y = 0; y < height; y++) { + 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 + } + } + } else { + // 垂直方向:从下到上 for (let y = 0; y < height; y++) { - 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 + 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 + } } } @@ -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) => { const canvas = canvasRef.current if (!canvas) return const rect = canvas.getBoundingClientRect() - const x = e.clientX - rect.left - const t = x / width + let t: number + + if (orientation === 'horizontal') { + const x = e.clientX - rect.left + 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 ( -
+
{showLabels && ( -
- {formatValue(minValue)} - {formatValue(maxValue)} -
+ orientation === 'horizontal' ? ( +
+ {formatValue(minValue)} + {formatValue(maxValue)} +
+ ) : ( +
+ {formatValue(maxValue)} + {formatValue(minValue)} +
+ ) )} {isHovered && hoverValue !== null && (
{formatValue(hoverValue)}
diff --git a/components/map-component.tsx b/components/map-component.tsx index 14a8d14..6d7c1f7 100644 --- a/components/map-component.tsx +++ b/components/map-component.tsx @@ -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(false) const [currentColorMapType, setCurrentColorMapType] = useState(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 (
- {/* Colorbar 在右下角 */} -
-
色标
+ {/* 可拖动的 Colorbar */} +
diff --git a/hooks/use-timeline.ts b/hooks/use-timeline.ts index 1d2ec9a..887d447 100644 --- a/hooks/use-timeline.ts +++ b/hooks/use-timeline.ts @@ -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(null) const { setTime, currentDatetime, setTimelineTime, timelineDatetime } = useMap() + const [wsStatus, setWsStatus] = useState(WsStatus.DISCONNECTED) const updateDate = useCallback((newDate: Date) => { setTime(newDate) @@ -132,6 +139,8 @@ export function useTimeline({ changeSpeed, jumpToDate, updateDate, - setTime + setTime, + wsStatus, + setWsStatus } } \ No newline at end of file diff --git a/lib/color-maps.ts b/lib/color-maps.ts index 1c0a2b0..04b6284 100644 --- a/lib/color-maps.ts +++ b/lib/color-maps.ts @@ -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]; - } + // 使用固定颜色,不进行插值 + r = range.color[0]; + g = range.color[1]; + b = range.color[2]; found = true; break; } diff --git a/wind.glsl b/wind.glsl index 3eb3776..3a3a387 100644 --- a/wind.glsl +++ b/wind.glsl @@ -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); } \ No newline at end of file