'use client' import React, { useEffect, useRef, useState, useCallback } 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' import { Loader2 } from 'lucide-react' 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) { // 使用完整的 useRadarTile 功能,包括缓存和预取 const { isLoading, switchToTime, currentBitmap, isWorkerSupported } = useRadarTile({ maxBufferSize: 16, // 增大缓存以支持时间序列 preCacheCount: 3, generateUrl: (timestamp: number) => { const utc_time_str = formatInTimeZone(timestamp, 'UTC', 'yyyyMMddHHmmss') const new_url_prefix = process.env.NEXT_PUBLIC_GRAPHQL_BACKEND_URL?.replace('/graphql', '') || 'http://localhost:3050' return `${new_url_prefix}/api/v1/data?datetime=${utc_time_str}&area=cn` } }); const mapContainer = useRef(null) const { setMap, mapRef, currentDatetime, isMapReady, opacity } = useMap() const { location } = useMapLocation() const texRef = useRef(null) const lutTexRef = useRef(null) const glRef = useRef(null) const customLayerRef = useRef(null) const [isReady, setIsReady] = useState(false) const [currentColorMapType, setCurrentColorMapType] = useState(colorMapType) const isImageBitmapLoaded = useRef(false) // 缓存统计状态 const [cacheStats, setCacheStats] = useState({ totalFrames: 0, loadedFrames: 0, cacheHitRate: 0 }) const [showCacheStats, setShowCacheStats] = useState(false) // 拖动状态 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; switchToTime(currentDatetime.getTime()) }, [currentDatetime, isMapReady]) // 键盘快捷键切换缓存统计显示 useEffect(() => { const handleKeyPress = (e: KeyboardEvent) => { // Ctrl/Cmd + Shift + C 切换缓存统计显示 if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'C') { e.preventDefault() setShowCacheStats(prev => !prev) logger.debug(`Cache stats display ${!showCacheStats ? 'enabled' : 'disabled'}`, { component: 'MapComponent' }); } } document.addEventListener('keydown', handleKeyPress) return () => document.removeEventListener('keydown', handleKeyPress) }, [showCacheStats]) 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, // 缓存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'), 'u_opacity': gl.getUniformLocation(program, 'u_opacity') }; // 创建并绑定顶点缓冲区 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); } if (locations['u_opacity']) { gl.uniform1f(locations['u_opacity'], opacity.current / 100); } 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 () => { logger.info('Cleaning up map and radar tile resources', { component: 'MapComponent' }); // 清理自定义图层引用 customLayerRef.current = null; // 清理 WebGL 引用 glRef.current = null; texRef.current = null; lutTexRef.current = null; // 重置状态 setIsReady(false); setCacheStats({ totalFrames: 0, loadedFrames: 0, cacheHitRate: 0 }) // 移除地图实例 if (map) { map.remove(); } // 结束Sentry span // span.end(); } }, [mapContainer]) useEffect(() => { if (currentBitmap && texRef.current) { const gl = glRef.current if (!gl) return; gl.bindTexture(gl.TEXTURE_2D, texRef.current) // 针对灰度图优化:使用单通道RED格式,减少内存使用和提高性能 // 虽然ImageBitmap仍是RGBA格式,但WebGL会自动将灰度值映射到RED通道 if (!isImageBitmapLoaded.current) { gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, // 内部格式:使用RGBA,兼容性更好 gl.RGBA, // 数据格式:ImageBitmap总是RGBA gl.UNSIGNED_BYTE, currentBitmap ) isImageBitmapLoaded.current = true } else { gl.texSubImage2D( gl.TEXTURE_2D, 0, 0, 0, currentBitmap.width, currentBitmap.height, gl.RGBA, gl.UNSIGNED_BYTE, currentBitmap ) } // 设置纹理参数(如果还没有设置) 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() } }, [currentBitmap, 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 (
{/* 缓存统计信息显示 */} {showCacheStats && (
Radar Cache Stats
Total Frames: {cacheStats.totalFrames}
Loaded: {cacheStats.loadedFrames}
Hit Rate: {Math.round(cacheStats.cacheHitRate * 100)}%
Worker: {isWorkerSupported ? '✓' : '✗'}
Ctrl+Shift+C to toggle
)} {isLoading && (
)}
) } 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; // 缓存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; }