Compare commits
No commits in common. "bc81aeec1d0a14f58591014788391bbad1fabaa7" and "a88dfe19140340ece9c377c8d7a8d96ea6d24f0f" have entirely different histories.
bc81aeec1d
...
a88dfe1914
@ -1,46 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
#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);
|
||||
}
|
||||
20
app/page.tsx
20
app/page.tsx
@ -22,8 +22,6 @@ 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'
|
||||
|
||||
|
||||
|
||||
@ -32,18 +30,14 @@ 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>
|
||||
<div className="flex-shrink-0">
|
||||
<Timeline />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 min-h-0">
|
||||
<MapComponent />
|
||||
</div>
|
||||
</WSProvider>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Timeline />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
18
app/tl.tsx
18
app/tl.tsx
@ -10,8 +10,19 @@ 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 { useWS } from "./ws-context";
|
||||
import { gql, useSubscription } from "@apollo/client";
|
||||
|
||||
const SUBSCRIPTION_QUERY = gql`
|
||||
subscription {
|
||||
statusUpdates {
|
||||
id
|
||||
message
|
||||
status
|
||||
timestamp
|
||||
newestDt
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
interface Uniforms {
|
||||
startTimestamp: number; // Unix 时间戳开始
|
||||
@ -68,7 +79,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
timelineConfig,
|
||||
...props
|
||||
}) => {
|
||||
const { isPlaying, togglePlay, setTime, setTimelineTime, setWsStatus } = useTimeline({})
|
||||
const { isPlaying, togglePlay, currentDatetime, setTime, setTimelineTime, timelineDatetime } = useTimeline({})
|
||||
const [lock, setLock] = useState(false)
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -81,7 +92,8 @@ export const Timeline: React.FC<Props> = React.memo(({
|
||||
currentLevel: null as any
|
||||
});
|
||||
|
||||
const { data } = useWS()
|
||||
|
||||
const { data, loading, error } = useSubscription(SUBSCRIPTION_QUERY)
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
'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
|
||||
}
|
||||
@ -11,7 +11,6 @@ interface ColorbarProps {
|
||||
minValue?: number
|
||||
maxValue?: number
|
||||
unit?: string
|
||||
orientation?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
export function Colorbar({
|
||||
@ -21,8 +20,7 @@ export function Colorbar({
|
||||
showLabels = true,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
unit = '',
|
||||
orientation = 'horizontal'
|
||||
unit = ''
|
||||
}: ColorbarProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
@ -45,43 +43,21 @@ 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
|
||||
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 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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,23 +68,15 @@ export function Colorbar({
|
||||
ctx.lineWidth = 1
|
||||
ctx.strokeRect(0, 0, width, height)
|
||||
|
||||
}, [colorMapType, width, height, orientation])
|
||||
}, [colorMapType, width, height])
|
||||
|
||||
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
|
||||
t = x / width
|
||||
} else {
|
||||
const y = e.clientY - rect.top
|
||||
t = (height - y) / height // 垂直方向:从下到上
|
||||
}
|
||||
|
||||
const x = e.clientX - rect.left
|
||||
const t = x / width
|
||||
const value = minValue + t * (maxValue - minValue)
|
||||
setHoverValue(value)
|
||||
setIsHovered(true)
|
||||
@ -132,7 +100,7 @@ export function Colorbar({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={orientation === 'horizontal' ? 'relative' : 'flex items-center'}>
|
||||
<div className="relative">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="cursor-crosshair"
|
||||
@ -142,33 +110,20 @@ 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>
|
||||
)
|
||||
<div className="flex justify-between text-xs text-gray-600 mt-1">
|
||||
<span>{formatValue(minValue)}</span>
|
||||
<span>{formatValue(maxValue)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isHovered && hoverValue !== null && (
|
||||
<div
|
||||
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%)'
|
||||
}
|
||||
}
|
||||
className="absolute bg-black text-white text-xs px-2 py-1 rounded pointer-events-none z-10"
|
||||
style={{
|
||||
left: `${(hoverValue - minValue) / (maxValue - minValue) * width}px`,
|
||||
top: '-30px',
|
||||
transform: 'translateX(-50%)'
|
||||
}}
|
||||
>
|
||||
{formatValue(hoverValue)}
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, LogoControl, } from 'maplibre-gl'
|
||||
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, } from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { useMap } from '@/app/map-context'
|
||||
import { useMapLocation } from '@/hooks/use-map-location'
|
||||
@ -9,8 +9,6 @@ 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
|
||||
@ -43,18 +41,6 @@ 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')
|
||||
@ -70,15 +56,12 @@ 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'
|
||||
@ -99,6 +82,87 @@ 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);
|
||||
@ -478,73 +542,6 @@ 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
|
||||
@ -553,27 +550,16 @@ export function MapComponent({
|
||||
style={{ minHeight: '400px' }}
|
||||
/>
|
||||
|
||||
{/* 可拖动的 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 在右下角 */}
|
||||
<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
|
||||
colorMapType={currentColorMapType}
|
||||
width={16}
|
||||
height={220}
|
||||
width={180}
|
||||
height={16}
|
||||
minValue={0}
|
||||
maxValue={75}
|
||||
unit="dBZ"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -24,12 +24,6 @@ interface UseTimelineOptions {
|
||||
autoUpdate?: boolean
|
||||
}
|
||||
|
||||
enum WsStatus {
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
DISCONNECTED = 'disconnected'
|
||||
}
|
||||
|
||||
export function useTimeline({
|
||||
startDate = subDays(new Date(), 30),
|
||||
endDate = new Date(),
|
||||
@ -41,7 +35,6 @@ 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)
|
||||
@ -139,8 +132,6 @@ export function useTimeline({
|
||||
changeSpeed,
|
||||
jumpToDate,
|
||||
updateDate,
|
||||
setTime,
|
||||
wsStatus,
|
||||
setWsStatus
|
||||
setTime
|
||||
}
|
||||
}
|
||||
@ -266,7 +266,7 @@ 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]) {
|
||||
// 使用固定颜色,不进行插值
|
||||
// 使用区间的固定颜色,不进行插值
|
||||
r = range.color[0];
|
||||
g = range.color[1];
|
||||
b = range.color[2];
|
||||
|
||||
22
wind.glsl
22
wind.glsl
@ -63,24 +63,6 @@ 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));
|
||||
}
|
||||
@ -122,9 +104,9 @@ vec4 projectTileFor3D(vec2 posInTile,float elevation) {
|
||||
|
||||
#define GLOBE
|
||||
|
||||
out vec2 lonlat;
|
||||
out vec2 v_tex_coord;
|
||||
|
||||
void main() {
|
||||
gl_Position = projectTile(a_pos);
|
||||
lonlat = computeLatLon(a_pos);
|
||||
v_tex_coord = a_tex_coord;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user