geojson-bulk
本文回答:用
source.setData(FeatureCollection)做大数据批量绘制 —— 增量更新、diff、性能。
一、一次性全量 setData
map.addGeoJSONSource('pts', { type: 'FeatureCollection', features: [] });
map.addCircleLayer('pts-layer', 'pts', { circleColor: '#f00', circleRadius: 3 });
const features = [];
for (let i = 0; i < 100000; i++) {
features.push({
type: 'Feature',
id: i,
geometry: { type: 'Point', coordinates: map.toLngLat([Math.random()*1e6, Math.random()*1e6]) },
properties: { level: Math.floor(Math.random()*5) }
});
}
map.getSource('pts').setData({ type: 'FeatureCollection', features });
10 万点,setData 约 50–300 ms(取决于浏览器)。
二、增量更新(diff)
避免每次重建:
class IncrementalSource {
constructor(map, sourceId) {
this.map = map;
this.sourceId = sourceId;
this.features = new Map();
}
add(feat) { this.features.set(feat.id, feat); this._flush(); }
update(id, patch) {
const f = this.features.get(id);
if (!f) return;
Object.assign(f.properties, patch);
this._flush();
}
remove(id) { this.features.delete(id); this._flush(); }
_flush() {
if (this._pending) return;
this._pending = requestAnimationFrame(() => {
this.map.getSource(this.sourceId).setData({
type: 'FeatureCollection',
features: [...this.features.values()]
});
this._pending = null;
});
}
}
const src = new IncrementalSource(map, 'pts');
src.add(f1); src.add(f2); src.update(1, { level: 9 });
requestAnimationFrame 合并同一帧多次变更。
三、只更新 feature-state(最快)
仅改样式(颜色、透明度)时不要重建 source,用 feature-state:
map.setFeatureState(
{ source: 'pts', id: 123 },
{ selected: true }
);
// 图层样式用表达式读取
map.addCircleLayer('pts-layer', 'pts', {
circleColor: [
'case',
['boolean', ['feature-state', 'selected'], false], '#f00',
'#888'
]
});
四、聚合(cluster)
// 签名:addGeoJSONSource(id, geojsonData, props?: GeoJSONSourceSpecification)
// cluster 系列选项在第三个参数(source 属性),不是塞进 FeatureCollection
map.addGeoJSONSource('pts',
{ type: 'FeatureCollection', features },
{
cluster: true,
clusterRadius: 40,
clusterMaxZoom: 14,
clusterProperties: {
sumLevel: ['+', ['get', 'level']]
}
});
展示集群圆 + 点击展开:
map.addCircleLayer('clusters', 'pts', {
circleColor: ['step', ['get', 'point_count'], '#0f0', 50, '#ff0', 200, '#f00'],
circleRadius: ['step', ['get', 'point_count'], 15, 50, 22, 200, 30],
filter: ['has', 'point_count']
});
map.addCircleLayer('unclustered', 'pts', {
circleColor: '#0af',
filter: ['!', ['has', 'point_count']]
});
map.on('click', 'clusters', e => {
const f = e.features[0];
// cluster 要素的聚合 id 在 properties.cluster_id(不是 f.id);
// getClusterExpansionZoom 是回调式 API,不返回 Promise
map.getSource('pts').getClusterExpansionZoom(f.properties.cluster_id, (err, zoom) => {
if (err) return;
map.easeTo({ center: f.geometry.coordinates, zoom });
});
});
五、大数据分层
function tieredSource(all) {
return [
{ zoomMin: 0, zoomMax: 8, sample: all.filter((_,i) => i % 100 === 0) },
{ zoomMin: 8, zoomMax: 12, sample: all.filter((_,i) => i % 10 === 0) },
{ zoomMin: 12, zoomMax: 24, sample: all }
];
}
按 zoom 切换源的 setData,低 zoom 抽稀。
六、分批插入
100 万点一次 setData 会阻塞主线程:
function bulkInsert(all, batchSize = 20000) {
let i = 0;
function tick() {
const slice = all.slice(i, i + batchSize);
i += batchSize;
src.features.push(...slice);
src.setData({ type: 'FeatureCollection', features: src.features });
if (i < all.length) requestAnimationFrame(tick);
}
tick();
}
七、使用 promoteId 把 DB id 映射为 feature id
map.addGeoJSONSource('devices',
{ type: 'FeatureCollection', features },
{ promoteId: 'device_id' } // feature.properties.device_id → feature.id
);
这样 setFeatureState 可以用真实业务 id,不用人工维护。
八、性能数据参考
| 数据量 | setData 时间 | 内存 |
|---|---|---|
| 1 万点 | < 10 ms | ~5 MB |
| 10 万点 | 50–150 ms | ~50 MB |
| 100 万点 | 500 ms ~ 2 s | ~500 MB |
| 100 万点 + cluster | 首次 setData 2 ~ 5 s | 高 |
九、常见坑
- setData 每帧重建:浪费 GeoJSON worker 时间;diff + RAF 合并。
- 不设
promoteId,feature.id 缺失:setFeatureState命中不到;要么generateId: true要么promoteId。 - cluster 对 heatmap 不适用:cluster + heatmap 组合不生效;如需热力图直接用 heatmap 图层。
- bbox 过大的 feature:一个 Polygon 几千 ring 会严重拖慢 serialization;分割成多个 feature 更快。