一、问题背景

在地图应用中,经常需要展示海量的点位数据(如出租车、共享单车、监控点位等)。当数据量达到几万甚至几十万个点时,会带来以下问题:

  1. 渲染性能下降
  2. 交互响应迟缓
  3. 内存占用过大
  4. 视觉上的点位重叠

二、解决方案

1. 数据分层与分块

1.1 按层级分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LayerManager {
constructor() {
this.layers = new Map(); // 存储不同层级的数据
}

addPoint(point, zoom) {
if (!this.layers.has(zoom)) {
this.layers.set(zoom, new Set());
}
this.layers.get(zoom).add(point);
}

getVisiblePoints(zoom) {
return this.layers.get(Math.floor(zoom)) || new Set();
}
}

1.2 网格分块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class GridManager {
constructor(gridSize) {
this.gridSize = gridSize;
this.grids = new Map();
}

// 计算点位所在的网格
getGridKey(lat, lng) {
const x = Math.floor(lng / this.gridSize);
const y = Math.floor(lat / this.gridSize);
return `${x}-${y}`;
}

// 添加点位到网格
addPoint(point) {
const key = this.getGridKey(point.lat, point.lng);
if (!this.grids.has(key)) {
this.grids.set(key, []);
}
this.grids.get(key).push(point);
}

// 获取视野范围内的网格数据
getVisiblePoints(bounds) {
const visiblePoints = [];
const { north, south, east, west } = bounds;

for (let lat = south; lat <= north; lat += this.gridSize) {
for (let lng = west; lng <= east; lng += this.gridSize) {
const key = this.getGridKey(lat, lng);
const points = this.grids.get(key) || [];
visiblePoints.push(...points);
}
}

return visiblePoints;
}
}

2. 点位聚合

2.1 基础聚合算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class ClusterManager {
constructor(radius) {
this.radius = radius; // 聚合半径
}

// 计算两点距离
getDistance(p1, p2) {
const dx = p1.lng - p2.lng;
const dy = p1.lat - p2.lat;
return Math.sqrt(dx * dx + dy * dy);
}

// 聚合点位
cluster(points) {
const clusters = [];
const processed = new Set();

for (const point of points) {
if (processed.has(point)) continue;

const cluster = {
center: point,
points: [point],
count: 1
};

for (const other of points) {
if (processed.has(other)) continue;

if (this.getDistance(point, other) <= this.radius) {
cluster.points.push(other);
cluster.count++;
processed.add(other);
}
}

clusters.push(cluster);
}

return clusters;
}
}

2.2 四叉树聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class QuadTree {
constructor(bounds, capacity) {
this.bounds = bounds;
this.capacity = capacity;
this.points = [];
this.divided = false;
}

// 划分区域
subdivide() {
const { x, y, width, height } = this.bounds;
const w = width / 2;
const h = height / 2;

this.northwest = new QuadTree({x, y, width: w, height: h}, this.capacity);
this.northeast = new QuadTree({x: x + w, y, width: w, height: h}, this.capacity);
this.southwest = new QuadTree({x, y: y + h, width: w, height: h}, this.capacity);
this.southeast = new QuadTree({x: x + w, y: y + h, width: w, height: h}, this.capacity);

this.divided = true;
}

// 插入点位
insert(point) {
if (!this.bounds.contains(point)) {
return false;
}

if (this.points.length < this.capacity) {
this.points.push(point);
return true;
}

if (!this.divided) {
this.subdivide();
}

return (
this.northwest.insert(point) ||
this.northeast.insert(point) ||
this.southwest.insert(point) ||
this.southeast.insert(point)
);
}
}

3. 渲染优化

3.1 Canvas 渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CanvasLayer {
constructor(map) {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.map = map;
}

// 绘制点位
drawPoints(points) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

for (const point of points) {
const pixel = this.map.latLngToContainerPoint(point);

this.ctx.beginPath();
this.ctx.arc(pixel.x, pixel.y, 4, 0, Math.PI * 2);
this.ctx.fillStyle = point.color || '#ff0000';
this.ctx.fill();
}
}

// 更新 Canvas 大小
resize() {
const size = this.map.getSize();
this.canvas.width = size.x;
this.canvas.height = size.y;
}
}

3.2 WebGL 渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class WebGLLayer {
constructor(map) {
this.canvas = document.createElement('canvas');
this.gl = this.canvas.getContext('webgl');
this.map = map;

this.initShaders();
this.initBuffers();
}

// 初始化着色器
initShaders() {
const vertexShader = `
attribute vec2 a_position;
uniform vec2 u_resolution;

void main() {
vec2 clipSpace = (a_position / u_resolution) * 2.0 - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
gl_PointSize = 4.0;
}
`;

const fragmentShader = `
precision mediump float;
uniform vec4 u_color;

void main() {
float dist = length(gl_PointCoord - vec2(0.5, 0.5));
if (dist > 0.5) {
discard;
}
gl_FragColor = u_color;
}
`;

// ... 编译和链接着色器的代码
}

// 渲染点位
render(points) {
const positions = new Float32Array(points.length * 2);

points.forEach((point, i) => {
const pixel = this.map.latLngToContainerPoint(point);
positions[i * 2] = pixel.x;
positions[i * 2 + 1] = pixel.y;
});

// ... WebGL 绘制代码
}
}

4. 数据调度优化

4.1 视野范围计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ViewportManager {
constructor(map) {
this.map = map;
}

// 获取当前视野范围
getBounds() {
const bounds = this.map.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();

return {
north: ne.lat,
south: sw.lat,
east: ne.lng,
west: sw.lng
};
}

// 判断点是否在视野内
isPointInView(point) {
const bounds = this.getBounds();
return (
point.lat <= bounds.north &&
point.lat >= bounds.south &&
point.lng <= bounds.east &&
point.lng >= bounds.west
);
}
}

4.2 异步加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class DataLoader {
constructor() {
this.cache = new Map();
this.loading = new Set();
}

// 异步加载数据
async loadTileData(x, y, z) {
const key = `${x}-${y}-${z}`;

if (this.cache.has(key)) {
return this.cache.get(key);
}

if (this.loading.has(key)) {
return new Promise(resolve => {
const checkCache = setInterval(() => {
if (this.cache.has(key)) {
clearInterval(checkCache);
resolve(this.cache.get(key));
}
}, 100);
});
}

this.loading.add(key);

try {
const data = await fetch(`/api/points?x=${x}&y=${y}&z=${z}`);
const points = await data.json();

this.cache.set(key, points);
this.loading.delete(key);

return points;
} catch (error) {
this.loading.delete(key);
throw error;
}
}
}

三、实践建议

  1. 数据处理

    • 预处理数据,提前计算聚合结果
    • 使用 Web Worker 处理大量数据
    • 采用增量加载策略
  2. 渲染优化

    • 优先使用 Canvas/WebGL 渲染
    • 实现图层缓存机制
    • 控制重绘频率
  3. 交互优化

    • 实现节流和防抖
    • 优化事件监听器
    • 添加加载提示

四、完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class BigDataMap {
constructor(container) {
this.map = new Map(container);
this.gridManager = new GridManager(0.01);
this.clusterManager = new ClusterManager(50);
this.canvasLayer = new CanvasLayer(this.map);
this.dataLoader = new DataLoader();

this.initEvents();
}

// 初始化事件
initEvents() {
this.map.on('moveend', this.throttle(this.update.bind(this), 100));
this.map.on('zoomend', this.throttle(this.update.bind(this), 100));
}

// 更新视图
async update() {
const bounds = this.map.getBounds();
const zoom = this.map.getZoom();

// 加载数据
const points = await this.dataLoader.loadTileData(
bounds.getWest(),
bounds.getSouth(),
zoom
);

// 网格分块
this.gridManager.clear();
points.forEach(point => this.gridManager.addPoint(point));

// 获取视野内的点
const visiblePoints = this.gridManager.getVisiblePoints(bounds);

// 点位聚合
const clusters = this.clusterManager.cluster(visiblePoints);

// 渲染
this.canvasLayer.drawPoints(clusters);
}

// 节流函数
throttle(fn, delay) {
let timer = null;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
}

参考文献