为 mapbox 图层添加一个 zIndex参数

651 阅读4分钟

一、需求

我们在使用地图添加图层时一般都会想要去控制图层顺序,常见的我们叠加卫星影像、路网、标注瓦片图层希望的叠加顺序从下往上依次为影像=>路网=>标注,这在使用openlayers 时十分简单,只需要指定TileLayerzIndex配置项即可;但是在mapbox-gl 中可以通过以下方式实现:

  1. 保证map.addLayer 的顺序,图层添加的顺序就是渲染的图层顺序(非 3d 图层)
  2. map.addLayer指定第二个参数 beforeId,意味着图层添加到哪个图层之前(之下)
  3. 通过map.movelayer 来调整图层顺序

但是需要注意的是当我们图层的数量一旦变多后,管理就会变得异常复杂,所有图层可能在某个操作删除,某些操作又会添加,而且我们以上的2和 3 方法都需要预先判断beforeId的图层是否存在。所以当我们存在大量图层时又需要调整多个图层的顺序时,图层的管理就是一门艺术了。

PS:这里提一下,mapbox 定位是专注于可视化,他的所有实现都是依托于 style和产品studio,一般不会存在手动管理大量图层,我们的使用属于一种非主流方式,但是对于业务系统来说,这又是一个普遍的需求。

二、方案

这篇文章只谈如何去扩展图层zIndex属性,通过指定图层的zIndex属性来保证图层的渲染顺序,大致的使用示例如下:

map.addSource('t1', {
  type: 'image',
  url: 'https://www.mapbox.com/images/foo.png',
  coordinates: [
    [-76.54, 39.18],
    [-76.52, 39.18],
    [-76.52, 39.17],
    [-76.54, 39.17]
  ]
});
map.addLayer({
  id: 'raster',
  type: 'raster',
  source: 't1',
  layout: {
    visibility: 'visible',
  },
  paint: {
    'raster-fade-duration': 0,
  },
  zIndex: 99,
});

三、技术细节

​ 要去扩展zIndex属性,首先需要了解mapbox-gl的图层顺序是如何实现的,因为mapbox-gl底层使用的是webgl渲染,所以他的内部实现和openlayers有本质不同。我们直接来看 Painter.render ,可以简单确定 mapbox-gl 的核心渲染共计 5 个 RenderPass,按顺序分别为:

  1. offscreenPass 按照 order 顺序渲染

    const layerIds = this.style.order;
    for (const layerId of layerIds) {
      const layer = this.style._layers[layerId];
      const sourceCache = style._getLayerSourceCache(layer);
      if (!layer.hasOffscreenPass() || layer.isHidden(this.transform.zoom)) continue;
    
      const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;
      if (!(layer.type === 'custom' || layer.isSky()) && !(coords && coords.length)) continue;
    
      this.renderLayer(this, sourceCache, layer, coords);
    }
    
  2. opaquePass 按照 order顺序倒序渲染,即从上到下进行渲染

    // Opaque pass ===============================================
    // Draw opaque layers top-to-bottom first.
    this.renderPass = 'opaque';
    
    if (!this.terrain) {
      for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) {
        const layer = this.style._layers[layerIds[this.currentLayer]];
        const sourceCache = style._getLayerSourceCache(layer);
        if (layer.isSky()) continue;
        const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;
    
        this._renderTileClippingMasks(layer, sourceCache, coords);
        this.renderLayer(this, sourceCache, layer, coords);
      }
    }
    
  3. skyPass 按照 order 顺序渲染

    // Sky pass ======================================================
    // Draw all sky layers bottom to top.
    // They are drawn at max depth, they are drawn after opaque and before
    // translucent to fail depth testing and mix with translucent objects.
    this.renderPass = 'sky';
    const isTransitioning = globeToMercatorTransition(this.transform.zoom) > 0.0;
    if ((isTransitioning || this.transform.projection.name !== 'globe') && this.transform.isHorizonVisible()) {
      for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) {
        const layer = this.style._layers[layerIds[this.currentLayer]];
        const sourceCache = style._getLayerSourceCache(layer);
        if (!layer.isSky()) continue;
        const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;
    
        this.renderLayer(this, sourceCache, layer, coords);
      }
    }
    
  4. translucentPass 按照 order 顺序渲染

    // Translucent pass ===============================================
    // Draw all other layers bottom-to-top.
    this.renderPass = 'translucent';
    
    this.currentLayer = 0;
    while (this.currentLayer < layerIds.length) {
      const layer = this.style._layers[layerIds[this.currentLayer]];
      const sourceCache = style._getLayerSourceCache(layer);
    
      this.renderLayer(this, sourceCache, layer, coords);
    
      ++this.currentLayer;
    }
    
  5. debugPass 由于 for in 在对象遍历时未规定顺序,所以 debugPass 渲染是无序的,当然对于我们来说 debugPass 一般不影响我们业务

    const layers = values(this.style._layers);
    layers.forEach((layer) => {
      const sourceCache = style._getLayerSourceCache(layer);
      if (sourceCache && !layer.isHidden(this.transform.zoom)) {
        if (!selectedSource || (selectedSource.getSource().maxzoom < sourceCache.getSource().maxzoom)) {
          selectedSource = sourceCache;
        }
      }
    });
    
    if (this.options.showTileBoundaries) {
      draw.debug(this, selectedSource, selectedSource.getVisibleCoordinates());
    }
    

了解完以上renderPass 后我们能直观的发现 this.style.order决定了图层渲染的顺序,并且我们在 style.addLayer 的 图层顺序处理的代码 style.moveLayer 中都能发现 style._order 的处理:

this._order.splice(index, 0, id);
this._layerOrderChanged = true;

基于此,我们可以实现以下 orderLayers 方法:

mapboxgl.Map.prototype.orderLayers = function(orderLayers = []) {
  // @ts-ignore
  const ly = map.style._layers;
  const layers = Object.keys(ly).map((key) => ly[key]);
  const ownLayers = [];
  const beforeLayers = [];
  orderLayers.forEach((id) => {
    if (id) {
      const l = layers.find((item) => item.id === id);
      if (l) {
        ownLayers.push(l);
      }
    }
  });
  layers.forEach((layer) => {
    const l = ownLayers.find((item) => item.id === layer.id);
    if (!l) {
      beforeLayers.push(layer);
    }
  });
  const allLayers = beforeLayers.concat(ownLayers);
  // @link github.com/mapbox/mapbox-gl-js/src/style/style.js
  map.style._checkLoaded();
  map.style._changed = true;
  map.style._order = allLayers.map((a) => a.id);
  map.style._layerOrderChanged = true;
  return allLayers;
}

仅仅实现这个其实还是不能很好的处理我们的需求,因为从需求来说我们在调用map.addLayer 的时候需要自动读取配置项自动内部处理,基于此我们需要修改 addLayerremoveLayer的原型方法:

const addLayer = mapboxgl.Map.prototype.addLayer;
const removeLayer = mapboxgl.Map.prototype.removeLayer;
mapboxgl.Map.prototype.addLayer = function(
    layerObject: AnyLayer & {
      zIndex?: number;
    },
    before?: string,
  ) {
    if (this._layers === undefined) {
      // eslint-disable-next-line no-underscore-dangle
      this._layers = [];
      this._sortLayers = [];
    }
    if (layerObject.id) {
      this._layers.push(layerObject);
    }
    if ('zIndex' in layerObject) {
      this._sortLayers.push(layerObject);
    }
    const args = addLayer.call(this, layerObject, before);
    const sortLayers = this._sortLayers.sort((a: any, b: any) => a.zIndex - b.zIndex);

    this.orderLayers(sortLayers.map((layer: AnyLayer) => layer.id));
    return args;
  }

mapboxgl.Map.prototype.removeLayer = function(id: string) {
  this._layers = this._layers.filter((layer) => layer.id !== id);
  this._sortLayers = this._sortLayers.filter((layer) => layer.id !== id);
  return removeLayer.call(this, id);
}

最终使用时的图层配置如下,此配置的zIndex的作用是保证了矢量底图永远在最下方,而路网图层保证压盖到矢量底图之上、标注图层保证在路网图层之上:

image-20230311223245098

通过此配置addLayer 的调用顺序不再决定图层的最终渲染顺序,渲染顺序将由 layer 配置中的 zIndex 属性决定。

一个完整的示例如下: