mosaicmap/components/map-component.tsx
2025-08-08 07:20:05 +08:00

583 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import React, { useEffect, useRef, useState } from 'react'
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'
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'
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)
useEffect(() => {
if (!isMapReady || !currentDatetime) return;
const utc_time_str = formatInTimeZone(currentDatetime, 'UTC', 'yyyyMMddHHmmss')
const new_url = `http://localhost:3050/api/v1/data?datetime=${utc_time_str}&area=cn`
fetchRadarTile(new_url)
}, [currentDatetime, isMapReady])
useEffect(() => {
if (!mapContainer.current) return
const map = new maplibregl.Map({
container: mapContainer.current,
style: style,
center: location.center,
zoom: location.zoom,
canvasContextAttributes: {
contextType: 'webgl2', // 请求 WebGL2
antialias: true // 打开多重采样抗锯齿
}
})
map.on('style.load', () => {
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;
}
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() {
vec4 texColor = texture(u_tex, v_tex_coord);
// 对于灰度图RGB通道通常相同取红色通道作为灰度值
float value = texColor.r * 3.4;
if (value < 0.07) {
fragColor= vec4(1.0,1.0,1.0,0.2);
return;
}
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
// 使用 LUT 进行颜色映射
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
// 添加一些透明度,使低值区域更透明
// float alpha = smoothstep(0.0, 0.1, value);
float alpha = 0.7;
fragColor = vec4(lutColor.rgb, alpha);
// fragColor = vec4(1.0,1.0,1.0,0.2);
}`
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.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) {
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');
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);
}
}
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);
})
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();
}
}
}, [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])
return (
<div className="relative w-full h-full" style={{ minHeight: '400px' }}>
<div
ref={mapContainer}
className="w-full h-full"
/>
{/* 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={180}
height={16}
minValue={0}
maxValue={75}
unit="dBZ"
/>
</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;
}