radar layer

This commit is contained in:
tsuki 2025-07-25 22:39:08 +08:00
parent 33af6ca468
commit 8e847d44a8
7 changed files with 1159 additions and 170 deletions

View File

@ -25,6 +25,8 @@ import {
} from "lucide-react"
import { cn } from '@/lib/utils';
import { useTimeline } from '@/hooks/use-timeline';
import { useEffect } from 'react'
import { useRadarTile } from '@/hooks/use-radartile'
export default function Page() {
@ -42,6 +44,11 @@ export default function Page() {
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
const { setTime } = useTimeline()
const { fetchRadarTile } = useRadarTile({})
useEffect(() => {
fetchRadarTile("http://127.0.0.1:3050/test")
}, [])
return (
<SidebarProvider>

View File

@ -707,95 +707,7 @@ export const Timeline: React.FC<Props> = ({
}
const gl = (canvasRef.current.getContext('webgl2', {
antialias: true, // 启用抗锯齿
alpha: true, // 启用alpha通道以支持透明度
premultipliedAlpha: false, // 不使用预乘alpha
depth: false, // 不需要深度缓冲
stencil: false, // 不需要模板缓冲
preserveDrawingBuffer: false // 不保留绘制缓冲区
}) as WebGL2RenderingContext);
if (!gl) {
console.error('WebGL2 not supported');
return;
}
const program = createProgram(gl);
if (!program) {
console.error('Failed to create program');
return;
}
// 绑定uniform buffer到着色器程序
const uniformBlockIndex = gl.getUniformBlockIndex(program, 'Uniforms');
if (uniformBlockIndex !== gl.INVALID_INDEX) {
gl.uniformBlockBinding(program, uniformBlockIndex, 0);
}
const vao = gl.createVertexArray();
if (!vao) {
console.error('Failed to create vertex array');
return;
}
gl.bindVertexArray(vao);
const vertex_bf = defaultVb(gl);
const { buffer: instants_bf, count: actualInstanceCount } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
const uniform_bf = defaultUb(gl, current_uniforms.current);
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
vaoRef.current = vao;
vertex_bfRef.current = vertex_bf;
uniform_bfRef.current = uniform_bf;
instants_bfRef.current = instants_bf;
programRef.current = program;
instants_countRef.current = actualInstanceCount; // 使用实际生成的实例数量
function render() {
gl.clearColor(0, 0, 0, 0); // 深灰背景,便于看到刻度
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vaoRef.current);
// 绑定uniform buffer
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
gl.bindVertexArray(null);
}
function updateUniforms(uniforms: Uniforms) {
gl.bindBuffer(gl.UNIFORM_BUFFER, uniform_bfRef.current!);
const uniformData = new Float32Array([
uniforms.startTimestamp,
uniforms.endTimestamp,
uniforms.currentTimestamp,
uniforms.radius,
uniforms.d,
uniforms.timelineStartX,
uniforms.timelineEndX,
0.0, // padding - 填充以对齐到8字节边界
uniforms.viewportSize[0],
uniforms.viewportSize[1],
uniforms.zoomLevel,
uniforms.panOffset
]);
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
}
// TODO: 可以通过props传入自定义的时间轴刻度数据
// 或使用useImperativeHandle暴露更新方法
// 初始化uniform数据并渲染
updateUniforms(current_uniforms.current);
gl.viewport(0, 0, actualWidth, actualHeight);
render();
// 鼠标滚轮缩放和平移
const handleWheel = (e: WheelEvent) => {
@ -899,56 +811,7 @@ export const Timeline: React.FC<Props> = ({
// 添加全局鼠标抬起事件防止鼠标移出canvas后拖拽卡住
document.addEventListener('mouseup', handleMouseUp);
// 使用ResizeObserver监听canvas尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width: new_width, height: new_height } = entry.contentRect;
// 获取设备像素比例以支持高分屏
const dpr = window.devicePixelRatio || 1;
const actualWidth = Math.floor(new_width * dpr);
const actualHeight = Math.floor(new_height * dpr);
// 更新canvas的实际像素尺寸
canvasRef.current!.width = actualWidth;
canvasRef.current!.height = actualHeight;
// 设置CSS显示尺寸
canvasRef.current!.style.width = new_width + 'px';
canvasRef.current!.style.height = new_height + 'px';
// 更新刻度线canvas的尺寸
setupTicksCanvas(ticksCanvasRef.current!, new_width, new_height, dpr);
// ResizeObserver中需要立即重绘因为setupTicksCanvas会清空canvas
redraw();
// 更新uniform数据
current_uniforms.current.viewportSize = [actualWidth, actualHeight];
current_uniforms.current.radius = radius * dpr; // 调整radius以适应像素密度
current_uniforms.current.d = d * dpr; // 调整d以适应像素密度
current_uniforms.current.timelineStartX = 40 * dpr; // 时间轴开始坐标
current_uniforms.current.timelineEndX = (new_width - 40) * dpr; // 时间轴结束坐标
current_uniforms.current.zoomLevel = stateRef.current.zoomLevel;
current_uniforms.current.panOffset = stateRef.current.panOffset * dpr;
// 重新生成实例数据以适应新的canvas尺寸
const { buffer: new_instants_bf, count: new_count } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
// 更新实例buffer引用和数量
if (instants_bfRef.current) {
gl.deleteBuffer(instants_bfRef.current);
}
instants_bfRef.current = new_instants_bf;
instants_countRef.current = new_count;
updateUniforms(current_uniforms.current);
gl.viewport(0, 0, actualWidth, actualHeight);
render();
}
});
resizeObserver.observe(canvasRef.current!);
return () => {
// 移除事件监听器
@ -980,14 +843,7 @@ export const Timeline: React.FC<Props> = ({
// 重置临时状态不需要调用setState因为组件即将卸载或重新初始化
panTempOffsetRef.current = 0;
resizeObserver.disconnect();
// 清理WebGL资源
if (vaoRef.current) gl.deleteVertexArray(vaoRef.current);
if (vertex_bfRef.current) gl.deleteBuffer(vertex_bfRef.current);
if (uniform_bfRef.current) gl.deleteBuffer(uniform_bfRef.current);
if (instants_bfRef.current) gl.deleteBuffer(instants_bfRef.current);
if (programRef.current) gl.deleteProgram(programRef.current);
}
}, [boxSize, actualVesicaData]);
@ -1043,8 +899,115 @@ export const Timeline: React.FC<Props> = ({
gl.bindVertexArray(null);
}
}
}, [state.zoomLevel, state.panOffset, startDate, endDate, currentDate]);
useEffect(() => {
if (!canvasRef.current) return;
const dpr = window.devicePixelRatio || 1;
const displayWidth = canvasRef.current.clientWidth;
const displayHeight = canvasRef.current.clientHeight;
// 设置WebGL canvas的实际像素尺寸考虑高分屏
const actualWidth = Math.floor(displayWidth * dpr);
const actualHeight = Math.floor(displayHeight * dpr);
canvasRef.current.width = actualWidth;
canvasRef.current.height = actualHeight;
const gl = (canvasRef.current.getContext('webgl2', {
antialias: true, // 启用抗锯齿
alpha: true, // 启用alpha通道以支持透明度
premultipliedAlpha: false, // 不使用预乘alpha
depth: false, // 不需要深度缓冲
stencil: false, // 不需要模板缓冲
preserveDrawingBuffer: false // 不保留绘制缓冲区
}) as WebGL2RenderingContext);
if (!gl) {
console.error('WebGL2 not supported');
return;
}
const program = createProgram(gl);
if (!program) {
console.error('Failed to create program');
return;
}
// 绑定uniform buffer到着色器程序
const uniformBlockIndex = gl.getUniformBlockIndex(program, 'Uniforms');
if (uniformBlockIndex !== gl.INVALID_INDEX) {
gl.uniformBlockBinding(program, uniformBlockIndex, 0);
}
const vao = gl.createVertexArray();
if (!vao) {
console.error('Failed to create vertex array');
return;
}
gl.bindVertexArray(vao);
const vertex_bf = defaultVb(gl);
const { buffer: instants_bf, count: actualInstanceCount } = createVesicaInstances(gl, actualVesicaData, actualWidth, actualHeight, dpr);
const uniform_bf = defaultUb(gl, current_uniforms.current);
gl.bindVertexArray(null);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindBuffer(gl.UNIFORM_BUFFER, null);
vaoRef.current = vao;
vertex_bfRef.current = vertex_bf;
uniform_bfRef.current = uniform_bf;
instants_bfRef.current = instants_bf;
programRef.current = program;
instants_countRef.current = actualInstanceCount; // 使用实际生成的实例数量
function render() {
gl.clearColor(0, 0, 0, 0); // 深灰背景,便于看到刻度
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vaoRef.current);
// 绑定uniform buffer
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
gl.bindVertexArray(null);
}
function updateUniforms(uniforms: Uniforms) {
gl.bindBuffer(gl.UNIFORM_BUFFER, uniform_bfRef.current!);
const uniformData = new Float32Array([
uniforms.startTimestamp,
uniforms.endTimestamp,
uniforms.currentTimestamp,
uniforms.radius,
uniforms.d,
uniforms.timelineStartX,
uniforms.timelineEndX,
0.0, // padding - 填充以对齐到8字节边界
uniforms.viewportSize[0],
uniforms.viewportSize[1],
uniforms.zoomLevel,
uniforms.panOffset
]);
gl.bufferData(gl.UNIFORM_BUFFER, uniformData, gl.DYNAMIC_DRAW);
}
// TODO: 可以通过props传入自定义的时间轴刻度数据
// 或使用useImperativeHandle暴露更新方法
// 初始化uniform数据并渲染
updateUniforms(current_uniforms.current);
gl.viewport(0, 0, actualWidth, actualHeight);
render();
}, [canvasRef.current]);
return (
<div className={cn(props.className, "w-full h-12 flex flex-row")}>
<div className="h-full flex flex-row items-center px-3 gap-2" style={{ boxShadow: '8px 0 24px rgba(0, 0, 0, 0.15), 4px 0 12px rgba(0, 0, 0, 0.1)' }}>

View File

@ -1,20 +1,12 @@
'use client'
import React, { useEffect, useRef } from 'react'
import maplibregl, { ProjectionDefinition } from 'maplibre-gl'
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, createTileMesh, Projection } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { useMap } from '@/app/map-context'
import { apply, applyStyle } from 'ol-mapbox-style';
import { useTheme } from '@/components/theme-provider'
// import TileWMS from 'ol/source/TileWMS.js';
// import Map from 'ol/Map';
// import View from 'ol/View';
// import TileLayer from 'ol/layer/Tile';
// import { transformExtent, fromLonLat } from 'ol/proj.js';
// import StadiaMaps from 'ol/source/StadiaMaps.js';
// import XYZ from 'ol/source/XYZ';
// import 'ol/ol.css';
import { useMapLocation } from '@/hooks/use-map-location'
import { createOptimalWorldMesh, getSubdivisionRecommendation, detectPerformanceLevel, createOptimalRegionMesh, RegionMeshPresets } from '@/lib/tile-mesh'
import { useRadarTile } from '@/hooks/use-radartile'
interface MapComponentProps {
style?: string
@ -32,6 +24,10 @@ export function MapComponent({
const mapContainer = useRef<HTMLDivElement>(null)
const { setMap } = useMap()
const { location } = useMapLocation()
const { radarTileRef } = useRadarTile()
const texRef = useRef<WebGLTexture | null>(null)
const lutTexRef = useRef<WebGLTexture | null>(null)
const glRef = useRef<WebGL2RenderingContext | null>(null)
useEffect(() => {
if (!mapContainer.current) return
@ -79,21 +75,343 @@ export function MapComponent({
type: 'globe'
})
map.addSource('nexrad', {
type: 'raster',
tiles: [
// 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?service=WMS&version=1.3.0&request=GetMap&layers=nexrad-n0r-wmst&styles=&format=image/png&transparent=true&crs=EPSG:3857&bbox={bbox-epsg-3857}&width=256&height=256'
'http://127.0.0.1:3050/tiles/{z}/{x}/{y}?time=202507220012'
],
tileSize: 256
});
// map.addSource('nexrad', {
// type: 'raster',
// tiles: [
// // 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?service=WMS&version=1.3.0&request=GetMap&layers=nexrad-n0r-wmst&styles=&format=image/png&transparent=true&crs=EPSG:3857&bbox={bbox-epsg-3857}&width=256&height=256'
// 'http://127.0.0.1:3050/tiles/{z}/{x}/{y}?time=202507220012'
// ],
// tileSize: 256
// });
map.addLayer({
id: 'nexrad-layer',
type: 'raster',
source: 'nexrad',
paint: { 'raster-opacity': 0.8 }
});
const customGlLayer: CustomGlLayer = {
id: 'player',
type: 'custom',
lastZoom: -1, // 添加缓存的缩放级别
uniformLocations: {} as Record<string, WebGLUniformLocation | null>, // 缓存uniform位置
prerender(gl: WebGLRenderingContext | WebGL2RenderingContext, { shaderData }: CustomRenderMethodInput) {
if (!this.program) {
glRef.current = gl as WebGL2RenderingContext;
if (!(gl instanceof WebGL2RenderingContext)) {
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_tex_coord;
void main() {
gl_Position = projectTile(a_pos);
v_tex_coord = a_tex_coord;
}`;
// WebGL2 fragment shader
const fragmentSource = `#version 300 es
precision highp float;
uniform sampler2D u_tex;
uniform sampler2D u_lut;
out vec4 fragColor;
in vec2 v_tex_coord;
void main() {
float value = texture(u_tex, v_tex_coord).r;
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
fragColor = lutColor;
}`
console.log(vertexSource, fragmentSource)
// Helper function to compile shader
const compileShader = (source: string, type: number): WebGLShader | null => {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
// Compile shaders
const vertexShader = compileShader(vertexSource, gl.VERTEX_SHADER);
const fragmentShader = compileShader(fragmentSource, gl.FRAGMENT_SHADER);
if (!vertexShader || !fragmentShader) return;
// Create and link program
const program = gl.createProgram();
if (!program) return;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
return;
}
// Clean up shaders (they're now part of the program)
gl.deleteShader(vertexShader);
gl.deleteShader(fragmentShader);
this.program = program;
const tex = gl.createTexture()
if (!tex) {
console.error('Failed to create texture');
return;
}
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
this.tex = tex;
texRef.current = tex;
// 创建 LUT 纹理
const lutTex = createLutTexture(gl);
if (!lutTex) return;
this.lutTex = lutTex;
lutTexRef.current = lutTex;
// 缓存uniform位置
this.uniformLocations = {
'u_projection_fallback_matrix': gl.getUniformLocation(program, 'u_projection_fallback_matrix'),
'u_projection_matrix': gl.getUniformLocation(program, 'u_projection_matrix'),
'u_projection_tile_mercator_coords': gl.getUniformLocation(program, 'u_projection_tile_mercator_coords'),
'u_projection_clipping_plane': gl.getUniformLocation(program, 'u_projection_clipping_plane'),
'u_projection_transition': gl.getUniformLocation(program, 'u_projection_transition'),
'u_tex': gl.getUniformLocation(program, 'u_tex'),
'u_lut': gl.getUniformLocation(program, 'u_lut')
};
// 创建并绑定顶点缓冲区
const vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
console.error('Failed to create vertex buffer');
return;
}
// 创建并绑定索引缓冲区
const indexBuffer = gl.createBuffer();
if (!indexBuffer) {
console.error('Failed to create index buffer');
return;
}
// Create vertex array object (WebGL2 feature)
const vao = gl.createVertexArray();
if (!vao) {
console.error('Failed to create VAO');
return;
}
gl.bindVertexArray(vao);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 设置位置属性 (location = 0)
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(
0, // attribute location
2, // size (x, y)
gl.FLOAT, // type
false, // normalized
16, // stride (4 floats * 4 bytes = 16 bytes per vertex)
0 // offset (位置在开始)
);
gl.vertexAttribDivisor(0, 0);
// 设置纹理坐标属性 (location = 1)
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(
1, // attribute location
2, // size (u, v)
gl.FLOAT, // type
false, // normalized
16, // stride (4 floats * 4 bytes = 16 bytes per vertex)
8 // offset (纹理坐标在位置之后2 floats * 4 bytes = 8 bytes)
);
gl.vertexAttribDivisor(1, 0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
// Unbind VAO
gl.bindVertexArray(null);
this.vao = vao;
this.vertexBuffer = vertexBuffer;
this.indexBuffer = indexBuffer;
}
// 只在缩放级别变化时更新网格数据
const currentZoom = Math.floor(map.getZoom());
if (currentZoom !== this.lastZoom) {
console.log(`缩放级别变化: ${this.lastZoom} -> ${currentZoom}`);
// 智能计算最佳细分数量
const performanceLevel = detectPerformanceLevel();
const canvas = map.getCanvas();
const viewportSize = canvas ? {
width: canvas.width,
height: canvas.height
} : undefined;
// 获取细分建议信息
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
console.log(`缩放级别: ${currentZoom}, 性能等级: ${performanceLevel}`);
console.log(`细分建议: ${recommendation.subdivisions} (${recommendation.description})`);
console.log(`三角形数量: ${recommendation.triangleCount}, 预计内存: ${recommendation.estimatedMemoryMB}MB`);
const meshData = RegionMeshPresets.china(currentZoom, 32);
if (gl instanceof WebGL2RenderingContext && this.vertexBuffer && this.indexBuffer) {
// 更新顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, meshData.vertices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 更新索引缓冲区
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, meshData.indices, gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
this.meshData = meshData;
this.lastZoom = currentZoom;
}
}
},
onAdd: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
console.log('Custom layer added');
},
onRemove: function (map: maplibregl.Map, gl: WebGL2RenderingContext) {
// 清理WebGL资源
if (this.program) {
if (gl) {
// 禁用顶点属性
gl.disableVertexAttribArray(0); // 位置属性
gl.disableVertexAttribArray(1); // 纹理坐标属性
gl.deleteProgram(this.program);
if (this.vertexBuffer) gl.deleteBuffer(this.vertexBuffer);
if (this.indexBuffer) gl.deleteBuffer(this.indexBuffer);
if (this.vao) gl.deleteVertexArray(this.vao);
if (this.tex) gl.deleteTexture(this.tex);
if (this.lutTex) gl.deleteTexture(this.lutTex);
}
}
console.log('Custom layer resources cleaned up');
},
render(gl: WebGL2RenderingContext | WebGLRenderingContext, { defaultProjectionData }: CustomRenderMethodInput) {
if (!(gl instanceof WebGL2RenderingContext) || !this.program || !this.meshData || !this.vao) {
return;
}
// 保存当前状态
const currentProgram = gl.getParameter(gl.CURRENT_PROGRAM);
const currentVAO = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
const blendEnabled = gl.isEnabled(gl.BLEND);
const currentBlendSrc = gl.getParameter(gl.BLEND_SRC_ALPHA);
const currentBlendDst = gl.getParameter(gl.BLEND_DST_ALPHA);
gl.useProgram(this.program);
// 使用缓存的uniform位置
const locations = this.uniformLocations!;
if (locations['u_projection_fallback_matrix']) {
gl.uniformMatrix4fv(
locations['u_projection_fallback_matrix'],
false,
defaultProjectionData.fallbackMatrix
);
}
if (locations['u_projection_matrix']) {
gl.uniformMatrix4fv(
locations['u_projection_matrix'],
false,
defaultProjectionData.mainMatrix
);
}
if (locations['u_projection_tile_mercator_coords']) {
gl.uniform4f(
locations['u_projection_tile_mercator_coords'],
...defaultProjectionData.tileMercatorCoords
);
}
if (locations['u_projection_clipping_plane']) {
gl.uniform4f(
locations['u_projection_clipping_plane'],
...defaultProjectionData.clippingPlane
);
}
if (locations['u_projection_transition']) {
gl.uniform1f(
locations['u_projection_transition'],
defaultProjectionData.projectionTransition
);
}
// 绑定纹理
if (this.tex && locations['u_tex']) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex);
gl.uniform1i(locations['u_tex'], 0);
}
if (this.lutTex && locations['u_lut']) {
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.lutTex);
gl.uniform1i(locations['u_lut'], 1);
}
gl.bindVertexArray(this.vao);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// 使用索引绘制三角形
const indexType = this.meshData.uses32bitIndices ? gl.UNSIGNED_INT : gl.UNSIGNED_SHORT;
const indexCount = this.meshData.indices.length;
gl.drawElements(gl.TRIANGLES, indexCount, indexType, 0);
// 恢复状态
gl.bindVertexArray(currentVAO);
gl.useProgram(currentProgram);
if (!blendEnabled) gl.disable(gl.BLEND);
if (blendEnabled) gl.blendFunc(currentBlendSrc, currentBlendDst);
}
};
map.addLayer(customGlLayer);
})
@ -101,6 +419,37 @@ export function MapComponent({
}, [mapContainer])
useEffect(() => {
if (radarTileRef.current.imgBitmap && texRef.current) {
const gl = glRef.current
if (!gl) return;
debugger
gl.bindTexture(gl.TEXTURE_2D, texRef.current)
// 针对灰度图优化使用单通道RED格式减少内存使用和提高性能
// 虽然ImageBitmap仍是RGBA格式但WebGL会自动将灰度值映射到RED通道
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RED, // 内部格式:单通道红色
gl.RGBA, // 数据格式ImageBitmap总是RGBA
gl.UNSIGNED_BYTE,
radarTileRef.current.imgBitmap
)
// 设置纹理参数(如果还没有设置)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
}
}, [radarTileRef.current.imgBitmap])
return (
<div
ref={mapContainer}
@ -108,4 +457,60 @@ export function MapComponent({
style={{ minHeight: '400px' }}
/>
)
}
}
interface CustomGlLayer extends CustomLayerInterface {
program?: WebGLProgram;
aPos?: number;
buffer?: WebGLBuffer | null;
vao?: WebGLVertexArrayObject | null;
meshData?: { vertices: Float32Array; indices: Uint16Array | Uint32Array; uses32bitIndices: boolean; vertexCount: number; triangleCount: number; };
vertexBuffer?: WebGLBuffer | null;
indexBuffer?: WebGLBuffer | null;
lastZoom?: number; // 缓存的缩放级别
uniformLocations?: Record<string, WebGLUniformLocation | null>; // 缓存uniform位置
tex?: WebGLTexture | null;
lutTex?: WebGLTexture | null;
}
function createLutTexture(gl: WebGL2RenderingContext) {
const lut = new Uint8Array(256 * 4);
for (let i = 0; i < 256; i++) {
lut[i * 4] = i;
lut[i * 4 + 1] = i;
lut[i * 4 + 2] = i;
lut[i * 4 + 3] = 255;
}
const tex = gl.createTexture()
if (!tex) {
console.error('Failed to create texture');
return;
}
gl.bindTexture(gl.TEXTURE_2D, tex)
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RGBA,
256,
1,
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
lut
)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null);
return tex;
}

56
hooks/use-radartile.ts Normal file
View File

@ -0,0 +1,56 @@
import { useState, useEffect, useRef, useCallback } from 'react'
import { addDays, subDays } from 'date-fns'
interface UseRadarTileOptions {
}
interface RadarTileStatus {
needRefresh: boolean;
isLoading: boolean;
isError: boolean;
url: string | null;
}
interface RadarTile {
imgBitmap: ImageBitmap | null;
needRefresh: boolean;
isLoading: boolean;
isError: boolean;
url: string | null;
}
export function useRadarTile({
}: UseRadarTileOptions = {}) {
const radarTileRef = useRef<RadarTile>({
imgBitmap: null,
needRefresh: false,
isLoading: false,
isError: false,
url: null,
})
const fetchRadarTile = useCallback(async (url: string) => {
radarTileRef.current.needRefresh = true
radarTileRef.current.isError = false
radarTileRef.current.url = url
}, [])
useEffect(() => {
if (radarTileRef.current.needRefresh) {
if (radarTileRef.current.url) {
fetch(radarTileRef.current.url).then(async (resp) => {
const blob = await resp.blob()
const imgBitmap = await createImageBitmap(blob)
radarTileRef.current.imgBitmap = imgBitmap
})
}
}
}, [radarTileRef.current.needRefresh, fetchRadarTile])
return {
radarTileRef,
fetchRadarTile,
}
}

446
lib/tile-mesh.ts Normal file
View File

@ -0,0 +1,446 @@
/**
*
* globe模式下的球面渲染
*/
export interface TileMeshOptions {
/** 经纬度边界 [west, south, east, north] */
bounds: [number, number, number, number];
/** 瓦片缩放级别 */
z: number;
/** 细分级别默认为瓦片级别的2倍 */
subdivisions?: number;
}
export interface TileMeshResult {
/** 顶点数据 (x, y, u, v) 坐标对:位置和纹理坐标 */
vertices: Float32Array;
/** 索引数据 */
indices: Uint16Array | Uint32Array;
/** 是否使用32位索引 */
uses32bitIndices: boolean;
/** 顶点数量 */
vertexCount: number;
/** 三角形数量 */
triangleCount: number;
}
/**
*
*/
export enum PerformanceLevel {
LOW = 'low', // 低端设备
MEDIUM = 'medium', // 中端设备
HIGH = 'high' // 高端设备
}
/**
*
*/
export function calculateOptimalSubdivisions(
zoomLevel: number,
performanceLevel: PerformanceLevel = PerformanceLevel.MEDIUM,
options?: {
/** 最小细分数量默认4 */
minSubdivisions?: number;
/** 最大细分数量默认64 */
maxSubdivisions?: number;
/** 是否为Globe模式Globe模式需要更多细分默认true */
isGlobeMode?: boolean;
/** 视口区域大小(像素),影响所需细分程度 */
viewportSize?: { width: number; height: number };
}
): number {
const {
minSubdivisions = 4,
maxSubdivisions = 64,
isGlobeMode = true,
viewportSize
} = options || {};
// 基础细分计算:随着缩放级别指数增长
let baseSubdivisions: number;
if (zoomLevel <= 2) {
// 非常低的缩放级别,使用最少细分
baseSubdivisions = minSubdivisions;
} else if (zoomLevel <= 6) {
// 低到中等缩放级别:线性增长
baseSubdivisions = minSubdivisions + (zoomLevel - 2) * 2;
} else if (zoomLevel <= 12) {
// 中等到高缩放级别:较快增长
baseSubdivisions = 12 + (zoomLevel - 6) * 3;
} else {
// 高缩放级别:平缓增长避免性能问题
baseSubdivisions = 30 + (zoomLevel - 12) * 1.5;
}
// 设备性能调整系数
const performanceMultipliers = {
[PerformanceLevel.LOW]: 0.6, // 低端设备减少40%细分
[PerformanceLevel.MEDIUM]: 1.0, // 中端设备保持基础细分
[PerformanceLevel.HIGH]: 1.4 // 高端设备增加40%细分
};
baseSubdivisions *= performanceMultipliers[performanceLevel];
// Globe模式调整球面渲染需要更多细分来避免失真
if (isGlobeMode) {
baseSubdivisions *= 1.2;
}
// 视口大小调整:大视口需要更多细分保证质量
if (viewportSize) {
const viewportArea = viewportSize.width * viewportSize.height;
const standardArea = 1920 * 1080; // 标准1080p面积
const sizeMultiplier = Math.sqrt(viewportArea / standardArea);
baseSubdivisions *= Math.min(1.5, Math.max(0.7, sizeMultiplier));
}
// 确保在合理范围内并且为2的幂次对GPU更友好
const clampedSubdivisions = Math.max(minSubdivisions, Math.min(maxSubdivisions, baseSubdivisions));
// 向最近的2的幂次取整4, 8, 16, 32, 64等
const powerOfTwo = Math.pow(2, Math.round(Math.log2(clampedSubdivisions)));
return Math.max(minSubdivisions, Math.min(maxSubdivisions, powerOfTwo));
}
/**
*
*/
export function detectPerformanceLevel(): PerformanceLevel {
if (typeof window === 'undefined') return PerformanceLevel.HIGH;
// 检查硬件并发数
const hardwareConcurrency = navigator.hardwareConcurrency || 4;
// 检查内存信息(如果可用)
const memory = (navigator as any).deviceMemory;
// 检查WebGL能力
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (!gl) return PerformanceLevel.LOW;
const renderer = gl.getParameter(gl.RENDERER) || '';
const vendor = gl.getParameter(gl.VENDOR) || '';
// 基于多个指标综合判断
let score = 0;
// CPU核心数评分
if (hardwareConcurrency >= 8) score += 3;
else if (hardwareConcurrency >= 4) score += 2;
else score += 1;
// 内存评分
if (memory) {
if (memory >= 8) score += 3;
else if (memory >= 4) score += 2;
else score += 1;
} else {
score += 2; // 默认中等
}
// GPU评分简化判断
if (renderer.toLowerCase().includes('intel')) {
score += 1; // 集成显卡通常性能较低
} else if (renderer.toLowerCase().includes('nvidia') || renderer.toLowerCase().includes('amd')) {
score += 3; // 独立显卡性能较好
} else {
score += 2; // 默认
}
// 根据总分判断性能等级
if (score <= 4) return PerformanceLevel.LOW;
else if (score <= 7) return PerformanceLevel.MEDIUM;
else return PerformanceLevel.HIGH;
}
/**
* Web Mercator坐标 (0-1)
*/
function lonLatToMercator(lon: number, lat: number): [number, number] {
const x = (lon + 180) / 360;
const latRad = (lat * Math.PI) / 180;
const y = (1 - Math.log(Math.tan(latRad / 2 + Math.PI / 4)) / Math.PI) / 2;
return [x, y];
}
/**
*
*/
export function createSubdividedTileMesh(options: TileMeshOptions): TileMeshResult {
const { bounds, z, subdivisions } = options;
const [west, south, east, north] = bounds;
// 根据瓦片级别确定细分级别
// 更高的瓦片级别需要更多的细分来在globe模式下保持平滑
const subdivLevel = subdivisions ?? Math.max(8, Math.min(32, Math.pow(2, Math.max(0, z - 5))));
// 将经纬度边界转换为归一化的Web Mercator坐标
const [mercWest, mercNorth] = lonLatToMercator(west, north);
const [mercEast, mercSouth] = lonLatToMercator(east, south);
// 创建顶点网格
const verticesPerRow = subdivLevel + 1;
const verticesPerCol = subdivLevel + 1;
const totalVertices = verticesPerRow * verticesPerCol;
// 每个顶点包含4个float值x, y, u, v (位置 + 纹理坐标)
const vertices = new Float32Array(totalVertices * 4);
// 生成顶点
for (let row = 0; row < verticesPerCol; row++) {
for (let col = 0; col < verticesPerRow; col++) {
const vertexIndex = (row * verticesPerRow + col) * 4;
// 在归一化空间中插值
const u = col / subdivLevel;
const v = row / subdivLevel;
// 计算实际的mercator坐标 (位置)
const x = mercWest + (mercEast - mercWest) * u;
const y = mercNorth + (mercSouth - mercNorth) * v;
// 设置顶点数据:位置(x, y) + 纹理坐标(u, v)
vertices[vertexIndex] = x; // 位置 x
vertices[vertexIndex + 1] = y; // 位置 y
vertices[vertexIndex + 2] = u; // 纹理坐标 u
vertices[vertexIndex + 3] = v; // 纹理坐标 v
}
}
// 创建三角形索引
const trianglesPerRow = subdivLevel;
const trianglesPerCol = subdivLevel;
const totalTriangles = trianglesPerRow * trianglesPerCol * 2;
const totalIndices = totalTriangles * 3;
// 判断是否需要32位索引
const uses32bitIndices = totalVertices > 65535;
const indices = uses32bitIndices
? new Uint32Array(totalIndices)
: new Uint16Array(totalIndices);
let indexOffset = 0;
// 生成三角形索引 (每个四边形分成两个三角形)
for (let row = 0; row < trianglesPerCol; row++) {
for (let col = 0; col < trianglesPerRow; col++) {
// 四边形的四个顶点索引
const topLeft = row * verticesPerRow + col;
const topRight = topLeft + 1;
const bottomLeft = (row + 1) * verticesPerRow + col;
const bottomRight = bottomLeft + 1;
// 第一个三角形 (左上角)
indices[indexOffset++] = topLeft;
indices[indexOffset++] = bottomLeft;
indices[indexOffset++] = topRight;
// 第二个三角形 (右下角)
indices[indexOffset++] = topRight;
indices[indexOffset++] = bottomLeft;
indices[indexOffset++] = bottomRight;
}
}
return {
vertices,
indices,
uses32bitIndices,
vertexCount: totalVertices,
triangleCount: totalTriangles
};
}
/**
*
*/
export function createSubdividedTileMeshFromTileCoords(
x: number,
y: number,
z: number,
subdivisions?: number
): TileMeshResult {
// 计算瓦片的经纬度边界
const n = Math.pow(2, z);
const west = (x / n) * 360 - 180;
const east = ((x + 1) / n) * 360 - 180;
const latRad1 = Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n)));
const latRad2 = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n)));
const north = (latRad1 * 180) / Math.PI;
const south = (latRad2 * 180) / Math.PI;
return createSubdividedTileMesh({
bounds: [west, south, east, north],
z,
subdivisions
});
}
/**
*
*/
export function createWorldSubdividedMesh(z: number, subdivisions?: number): TileMeshResult {
return createSubdividedTileMesh({
bounds: [-180, -85.0511, 180, 85.0511], // Web Mercator的纬度范围
z,
subdivisions
});
}
/**
* 使
*/
export function createRegionSubdividedMesh(
/** 中心经度 */
centerLon: number,
/** 中心纬度 */
centerLat: number,
/** 经度范围(度) */
lonRange: number,
/** 纬度范围(度) */
latRange: number,
/** 瓦片缩放级别 */
z: number,
/** 自定义细分级别 */
subdivisions?: number
): TileMeshResult {
const west = centerLon - lonRange / 2;
const east = centerLon + lonRange / 2;
const south = centerLat - latRange / 2;
const north = centerLat + latRange / 2;
return createSubdividedTileMesh({
bounds: [west, south, east, north],
z,
subdivisions
});
}
/**
*
*/
export const RegionMeshPresets = {
/** 新加坡区域 */
singapore: (z: number, subdivisions?: number) => createSubdividedTileMesh({
bounds: [103.6, 1.2, 104.0, 1.5],
z,
subdivisions
}),
/** 中国大陆区域 */
china: (z: number, subdivisions?: number) => createSubdividedTileMesh({
bounds: [73, 18, 135, 54],
z,
subdivisions
}),
/** 美国本土区域 */
usa: (z: number, subdivisions?: number) => createSubdividedTileMesh({
bounds: [-125, 25, -66, 49],
z,
subdivisions
}),
/** 欧洲区域 */
europe: (z: number, subdivisions?: number) => createSubdividedTileMesh({
bounds: [-10, 35, 40, 70],
z,
subdivisions
})
};
/**
* 便
*/
export function createOptimalWorldMesh(
zoomLevel: number,
performanceLevel?: PerformanceLevel,
options?: {
viewportSize?: { width: number; height: number };
isGlobeMode?: boolean;
minSubdivisions?: number;
maxSubdivisions?: number;
}
): TileMeshResult {
const detectedPerformance = performanceLevel ?? detectPerformanceLevel();
const optimalSubdivisions = calculateOptimalSubdivisions(
zoomLevel,
detectedPerformance,
options
);
return createWorldSubdividedMesh(zoomLevel, optimalSubdivisions);
}
/**
* 便
*/
export function createOptimalRegionMesh(
bounds: [number, number, number, number],
zoomLevel: number,
performanceLevel?: PerformanceLevel,
options?: {
viewportSize?: { width: number; height: number };
isGlobeMode?: boolean;
minSubdivisions?: number;
maxSubdivisions?: number;
}
): TileMeshResult {
const detectedPerformance = performanceLevel ?? detectPerformanceLevel();
const optimalSubdivisions = calculateOptimalSubdivisions(
zoomLevel,
detectedPerformance,
options
);
return createSubdividedTileMesh({
bounds,
z: zoomLevel,
subdivisions: optimalSubdivisions
});
}
/**
*
*/
export function getSubdivisionRecommendation(zoomLevel: number, performanceLevel: PerformanceLevel): {
subdivisions: number;
description: string;
triangleCount: number;
estimatedMemoryMB: number;
} {
const subdivisions = calculateOptimalSubdivisions(zoomLevel, performanceLevel);
const triangleCount = subdivisions * subdivisions * 2;
const vertexCount = (subdivisions + 1) * (subdivisions + 1);
// 估算内存使用 (顶点 + 索引)
const vertexMemory = vertexCount * 4 * 4; // 4个float32 per vertex
const indexMemory = triangleCount * 3 * (vertexCount > 65535 ? 4 : 2); // 3 indices per triangle
const estimatedMemoryMB = (vertexMemory + indexMemory) / (1024 * 1024);
let description: string;
if (subdivisions <= 8) {
description = "低细分 - 高性能,适合远距离视图";
} else if (subdivisions <= 16) {
description = "中等细分 - 平衡性能和质量";
} else if (subdivisions <= 32) {
description = "高细分 - 高质量,适合近距离视图";
} else {
description = "超高细分 - 最高质量,需要高端设备";
}
return {
subdivisions,
description,
triangleCount,
estimatedMemoryMB: Math.round(estimatedMemoryMB * 100) / 100
};
}

112
wind.glsl Normal file
View File

@ -0,0 +1,112 @@
#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));
}
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 v_tex_coord;
void main() {
gl_Position = projectTile(a_pos);
v_tex_coord = a_tex_coord;
}

View File