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