唯杰地图 logo唯杰地图
VJMAP
EN

geojson-bulk

vjmap/references/batch/geojson-bulk.md 5.0 KB182 行
原文

本文回答:用 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'
  ]
});

详见 primitive/geojson-source。

四、聚合(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 高

九、常见坑

  1. setData 每帧重建:浪费 GeoJSON worker 时间;diff + RAF 合并。
  2. 不设 promoteId,feature.id 缺失:setFeatureState 命中不到;要么 generateId: true 要么 promoteId。
  3. cluster 对 heatmap 不适用:cluster + heatmap 组合不生效;如需热力图直接用 heatmap 图层。
  4. bbox 过大的 feature:一个 Polygon 几千 ring 会严重拖慢 serialization;分割成多个 feature 更快。