712 lines
29 KiB
TypeScript
712 lines
29 KiB
TypeScript
'use client'
|
||
import React, { useEffect, useRef, useState } from 'react'
|
||
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'
|
||
import { getSubdivisionRecommendation, detectPerformanceLevel, RegionMeshPresets } from '@/lib/tile-mesh'
|
||
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'
|
||
import * as Sentry from '@sentry/nextjs'
|
||
import { logger, logWebGLError, logMapEvent, logPerformanceMetric } from '@/lib/logger'
|
||
|
||
interface MapComponentProps {
|
||
style?: string
|
||
center?: [number, number]
|
||
zoom?: number
|
||
imgBitmap?: ImageBitmap | null
|
||
colorMapType?: ColorMapType
|
||
onColorMapChange?: (type: ColorMapType) => void
|
||
}
|
||
|
||
export function MapComponent({
|
||
style = 'https://api.maptiler.com/maps/019817f1-82a8-7f37-901d-4bedf68b27fb/style.json?key=hj3fxRdwF9KjEsBq8sYI',
|
||
// style = 'https://api.maptiler.com/maps/landscape/style.json?key=hj3fxRdwF9KjEsBq8sYI',
|
||
// style = 'https://api.maptiler.com/tiles/land-gradient-dark/tiles.json?key=hj3fxRdwF9KjEsBq8sYI',
|
||
// center = [103.851959, 1.290270],
|
||
// zoom = 11
|
||
imgBitmap: propImgBitmap,
|
||
colorMapType = 'meteorological',
|
||
onColorMapChange
|
||
}: MapComponentProps) {
|
||
|
||
const { fetchRadarTile, imgBitmap } = useRadarTile();
|
||
const mapContainer = useRef<HTMLDivElement>(null)
|
||
const { setMap, mapRef, currentDatetime, isMapReady } = useMap()
|
||
const { location } = useMapLocation()
|
||
const texRef = useRef<WebGLTexture | null>(null)
|
||
const lutTexRef = useRef<WebGLTexture | null>(null)
|
||
const glRef = useRef<WebGL2RenderingContext | null>(null)
|
||
const customLayerRef = useRef<CustomGlLayer | null>(null)
|
||
const [isReady, setIsReady] = useState<boolean>(false)
|
||
const [currentColorMapType, setCurrentColorMapType] = useState<ColorMapType>(colorMapType)
|
||
|
||
// 拖动状态
|
||
const [colorbarPosition, setColorbarPosition] = useState({ x: 16, y: 36 }) // 从右边和下边的距离
|
||
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')
|
||
const new_url_prefix = process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL?.replace('/graphql', '') || 'http://localhost:3050'
|
||
const new_url = `${new_url_prefix}/api/v1/data?datetime=${utc_time_str}&area=cn`
|
||
fetchRadarTile(new_url)
|
||
}, [currentDatetime, isMapReady])
|
||
|
||
useEffect(() => {
|
||
if (!mapContainer.current) return
|
||
|
||
const span = Sentry.startInactiveSpan({
|
||
op: "ui.component.load",
|
||
name: "Map Component Initialization",
|
||
});
|
||
|
||
span.setAttribute("map.style", style);
|
||
span.setAttribute("map.center", `${location.center[0]},${location.center[1]}`);
|
||
span.setAttribute("map.zoom", location.zoom);
|
||
|
||
const map = new maplibregl.Map({
|
||
container: mapContainer.current,
|
||
style: style,
|
||
center: location.center,
|
||
zoom: location.zoom,
|
||
attributionControl: false, // 禁用默认的版权控制
|
||
canvasContextAttributes: {
|
||
contextType: 'webgl2', // 请求 WebGL2
|
||
antialias: true // 打开多重采样抗锯齿
|
||
},
|
||
})
|
||
|
||
|
||
|
||
map.on('style.load', () => {
|
||
logMapEvent('style.load', {
|
||
style: style,
|
||
center: location.center,
|
||
zoom: location.zoom
|
||
});
|
||
|
||
logger.info('Map style loaded successfully', {
|
||
component: 'MapComponent',
|
||
style: style
|
||
});
|
||
|
||
map.setProjection({
|
||
type: 'globe'
|
||
})
|
||
|
||
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;
|
||
}
|
||
|
||
// Helper function to compile shader
|
||
const compileShader = (source: string, type: number): WebGLShader | null => {
|
||
const shader = gl.createShader(type);
|
||
if (!shader) {
|
||
const error = new Error('Failed to create WebGL shader');
|
||
Sentry.captureException(error);
|
||
logWebGLError('shader_creation', 'Failed to create WebGL shader', { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' });
|
||
return null;
|
||
}
|
||
|
||
gl.shaderSource(shader, source);
|
||
gl.compileShader(shader);
|
||
|
||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||
const errorLog = gl.getShaderInfoLog(shader) || 'Unknown shader compilation error';
|
||
const error = new Error(`Shader compilation failed: ${errorLog}`);
|
||
Sentry.captureException(error, {
|
||
tags: { component: 'MapComponent', operation: 'shader_compilation' },
|
||
extra: { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment', source }
|
||
});
|
||
logWebGLError('shader_compilation', errorLog, { shaderType: type === gl.VERTEX_SHADER ? 'vertex' : 'fragment' });
|
||
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)) {
|
||
const errorLog = gl.getProgramInfoLog(program) || 'Unknown program linking error';
|
||
const error = new Error(`WebGL program linking failed: ${errorLog}`);
|
||
Sentry.captureException(error, {
|
||
tags: { component: 'MapComponent', operation: 'program_linking' },
|
||
extra: { programLog: errorLog }
|
||
});
|
||
logWebGLError('program_linking', errorLog);
|
||
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) {
|
||
const error = new Error('Failed to create WebGL texture');
|
||
Sentry.captureException(error, {
|
||
tags: { component: 'MapComponent', operation: 'texture_creation' }
|
||
});
|
||
logWebGLError('texture_creation', 'Failed to create WebGL texture');
|
||
return;
|
||
}
|
||
|
||
gl.bindTexture(gl.TEXTURE_2D, tex);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
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);
|
||
|
||
// 初始化时不更新纹理,等待 useEffect 中的更新
|
||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||
|
||
this.tex = tex;
|
||
texRef.current = tex;
|
||
|
||
// 创建 LUT 纹理
|
||
const lutTex = createLutTexture(gl, currentColorMapType);
|
||
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;
|
||
|
||
setIsReady(true)
|
||
}
|
||
|
||
// 只在缩放级别变化时更新网格数据
|
||
const currentZoom = Math.floor(map.getZoom());
|
||
if (currentZoom !== this.lastZoom) {
|
||
logMapEvent('zoom_change', {
|
||
previousZoom: this.lastZoom,
|
||
currentZoom: currentZoom
|
||
});
|
||
|
||
// 智能计算最佳细分数量
|
||
const performanceLevel = detectPerformanceLevel();
|
||
const canvas = map.getCanvas();
|
||
const viewportSize = canvas ? {
|
||
width: canvas.width,
|
||
height: canvas.height
|
||
} : undefined;
|
||
|
||
// 获取细分建议信息
|
||
const recommendation = getSubdivisionRecommendation(currentZoom, performanceLevel);
|
||
|
||
logger.debug(logger.fmt`Zoom level: ${currentZoom}, Performance level: ${performanceLevel}`, {
|
||
currentZoom,
|
||
performanceLevel,
|
||
component: 'MapComponent'
|
||
});
|
||
|
||
logger.debug(logger.fmt`Subdivision recommendation: ${recommendation.subdivisions} (${recommendation.description})`, {
|
||
subdivisions: recommendation.subdivisions,
|
||
description: recommendation.description,
|
||
triangleCount: recommendation.triangleCount,
|
||
estimatedMemoryMB: recommendation.estimatedMemoryMB
|
||
});
|
||
|
||
logPerformanceMetric('triangles', recommendation.triangleCount, 'count');
|
||
logPerformanceMetric('memory_estimate', 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) {
|
||
logger.info('WebGL custom layer added successfully', {
|
||
component: 'MapComponent',
|
||
layer: 'custom-gl-layer'
|
||
});
|
||
customLayerRef.current = this;
|
||
},
|
||
|
||
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);
|
||
}
|
||
}
|
||
logger.info('WebGL custom layer resources cleaned up successfully', {
|
||
component: 'MapComponent',
|
||
layer: 'custom-gl-layer'
|
||
});
|
||
},
|
||
|
||
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);
|
||
})
|
||
|
||
|
||
setMap(map, [])
|
||
|
||
// 清理函数:当组件卸载或重新初始化时清理资源
|
||
return () => {
|
||
// console.log('Cleaning up map resources...');
|
||
|
||
// 清理自定义图层引用
|
||
customLayerRef.current = null;
|
||
|
||
// 清理 WebGL 引用
|
||
glRef.current = null;
|
||
texRef.current = null;
|
||
lutTexRef.current = null;
|
||
|
||
// 重置状态
|
||
setIsReady(false);
|
||
|
||
// 移除地图实例
|
||
if (map) {
|
||
map.remove();
|
||
}
|
||
|
||
// 结束Sentry span
|
||
span.end();
|
||
}
|
||
|
||
}, [mapContainer])
|
||
|
||
useEffect(() => {
|
||
if (imgBitmap && texRef.current) {
|
||
const gl = glRef.current
|
||
if (!gl) return;
|
||
|
||
// console.log('Updating texture with imgBitmap:', imgBitmap);
|
||
|
||
gl.bindTexture(gl.TEXTURE_2D, texRef.current)
|
||
|
||
// 针对灰度图优化:使用单通道RED格式,减少内存使用和提高性能
|
||
// 虽然ImageBitmap仍是RGBA格式,但WebGL会自动将灰度值映射到RED通道
|
||
gl.texImage2D(
|
||
gl.TEXTURE_2D,
|
||
0,
|
||
gl.RGBA, // 内部格式:使用RGBA,兼容性更好
|
||
gl.RGBA, // 数据格式:ImageBitmap总是RGBA
|
||
gl.UNSIGNED_BYTE,
|
||
imgBitmap
|
||
)
|
||
|
||
// 设置纹理参数(如果还没有设置)
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
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);
|
||
|
||
// Redraw the map
|
||
mapRef.current?.triggerRepaint()
|
||
|
||
}
|
||
|
||
}, [imgBitmap, isReady])
|
||
|
||
// 监听色标类型变化,更新LUT纹理
|
||
useEffect(() => {
|
||
if (currentColorMapType !== colorMapType) {
|
||
setCurrentColorMapType(colorMapType);
|
||
}
|
||
}, [colorMapType, currentColorMapType])
|
||
|
||
// 当色标类型改变时,重新创建LUT纹理
|
||
useEffect(() => {
|
||
if (isReady && lutTexRef.current && glRef.current) {
|
||
const gl = glRef.current;
|
||
const newLutTex = createLutTexture(gl, currentColorMapType);
|
||
if (newLutTex) {
|
||
// 删除旧的纹理
|
||
gl.deleteTexture(lutTexRef.current);
|
||
lutTexRef.current = newLutTex;
|
||
|
||
// 通知自定义图层更新LUT纹理
|
||
if (customLayerRef.current) {
|
||
customLayerRef.current.lutTex = newLutTex;
|
||
}
|
||
}
|
||
}
|
||
}, [currentColorMapType, isReady])
|
||
|
||
// 拖动事件处理函数
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
Sentry.startSpan(
|
||
{
|
||
op: "ui.interaction",
|
||
name: "Colorbar Drag Start",
|
||
},
|
||
(span: any) => {
|
||
span.setAttribute("colorbar.position.x", colorbarPosition.x);
|
||
span.setAttribute("colorbar.position.y", colorbarPosition.y);
|
||
|
||
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
|
||
ref={mapContainer}
|
||
className="w-full h-full"
|
||
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
|
||
colorMapType={currentColorMapType}
|
||
width={16}
|
||
height={220}
|
||
minValue={0}
|
||
maxValue={75}
|
||
unit="dBZ"
|
||
orientation="vertical"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
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, colorMapType: ColorMapType = 'radar') {
|
||
// 使用统一的色标创建函数
|
||
const lut = createColorMap(colorMapType);
|
||
|
||
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.NEAREST);
|
||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||
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;
|
||
} |