mosaicmap/components/map-component.tsx
2025-07-25 22:39:08 +08:00

516 lines
21 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 } from 'react'
import maplibregl, { CustomLayerInterface, CustomRenderMethodInput, createTileMesh, Projection } from 'maplibre-gl'
import 'maplibre-gl/dist/maplibre-gl.css'
import { useMap } from '@/app/map-context'
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
center?: [number, number]
zoom?: number
}
// const extent = transformExtent([-126, 24, -66, 50], 'EPSG:4326', 'EPSG:3857');
export function MapComponent({
style = 'https://api.maptiler.com/maps/019817f1-82a8-7f37-901d-4bedf68b27fb/style.json?key=hj3fxRdwF9KjEsBq8sYI',
// center = [103.851959, 1.290270],
// zoom = 11
}: MapComponentProps) {
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
// const tileWmsLayer = new TileLayer({
// extent: extent,
// source: new TileWMS({
// attributions: ['Iowa State University'],
// url: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
// params: { 'LAYERS': 'nexrad-n0r-wmst' },
// }),
// opacity: 0.7,
// });
// const map = new Map({
// target: mapContainer.current,
// view: new View({
// center: fromLonLat(location.center),
// zoom: location.zoom,
// projection: 'EPSG:3857',
// showFullExtent: true,
// enableRotation: true
// }),
// })
// apply(map, style).then(() => {
// map.addLayer(tileWmsLayer)
// })
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'
})
// 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
// });
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);
})
setMap(map, [])
}, [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}
className="w-full h-full"
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;
}