跳到主要内容
Bran Zhang
返回

Mapbox 矢量瓦片的生命周期介绍

更新于:

本篇文章翻译自 maplibre/maplibre-gl-js 项目的《life-of-a-tile》。文中的超链接,对应到 Mapbox 的 v1.13.2 版本。

本篇文章将说明在 Mapbox 中,一个瓦片的加载流程,整个过程可以分为3个部分:

理想状况下,Event loop 和 Render loop 以60帧每秒的速度运转,类似 Tile loading 等重活,会放在 web worker 中异步执行。


Event Loop

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/event-loop.plantuml.svg

Transform

Transform 中保存了当前地图视角的信息(pitch, zoom, bearing, bounds 等)。代码中有2个地方能够直接更新 transform 的状态:

Camera 和 HandlerManager

Camera 和 HandlerManager 都可以在更新了 transform 的状态后,抛出 movezoommovestartmoveend 等事件。这些事件,包括其他的事件,例如样式更改或者数据加载完成的事件,都将触发对 Map#_render() 的调用,并渲染一帧地图画面。


Tile loading

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/fetch-tile.plantuml.svg

Map#_render() 会以2种不同的方式执行,取决于 map._sourcesDirty 的值。当 _sourcesDirty 为 true 时,_render() 首先要询问每个 source,是否需要请求新的数据。_sourcesDirty 为 false 时的情况将在下一节介绍。

请求

地图中的每个 source 对应着一个 sourceCache,调用这些 https://github.com/mapbox/mapbox-gl-js/blob/release-v1.13.2/src/source/source_cache.js#L474SourceCache#update(transform),得到理想状况下可以覆盖整个地图窗口范围的瓦片编号,如果存在没有请求过的瓦片,则发出请求。如果当前编号对应的瓦片不存在,则请求涵盖当前区域并且 z 值更小的瓦片。

解析

调用 Source#loadTile(tile, callback) 来加载缺失的瓦片,不同类型的 source 加载方式不一样:

RasterTileSource

通过 src/util/ajax 来触发 getImage 请求,src/util/ajax 维护着一个请求队列,用于控制同时进行中的请求数量。

RasterDEMTileSource

和上面的一样,首先请求图像数据,接着向 web worker 发出 loadDEMTile 消息。因为需要从图像数据中读取像素的信息,必须要将图像绘制到 canvas 上,这一步的性能开销比较大,所以当浏览器支持 OffscreenCanvas 时,这一步将在 web worker 中执行,否则直接在主线程上执行。

在 web worker 内,通过 RasterDEMTileWorkerSource#loadTile 将原始的 rgb 数据加载到一个 DEMData 实例中,并填充瓦片边缘 1px 的区域来避免闪烁的现象。最后将数据传回到主线程中。

VectorTileSource

向 web worker 发出 loadTile 或者 reloadTile 的消息,在 web worker 内,Worker#loadTile 收到消息并传递给 VectorTileWorkerSource#loadTile

调用 VectorTileWorkerSource#loadTile,内部的处理逻辑为:

调用 WorkerTile#parse()。在 web worker 里,根据瓦片的 id 接收上面的处理结果:

当所有的数据全都准备好时(通过 WorkerTile#maybePrepare() 判断),使用 potpack,将用到的字体,图标和图像构建成一个可以加载到 GPU 的正方形矩阵,这个正方形矩阵保存在 GlyphAtlasImageAtlas 对象中。之后对每个等待以上这些数据的图层,调用 StyleLayer#recalculate() 方法,并且:

将这些 buckets,featureIndex,collision boxes,glyphAtlas 和 imageAtlas 传回主线程。

GeojsonSource

和 VectorTileSource 几乎一样,向 web worker 发出 loadTile 或者 reloadTile 的消息,除了 GeojsonWorkerSource 继承 VectorTileWorkerSource 后重写了 loadVectorData 方法,因此不需要请求矢量瓦片并按照 pbf 规范解析,而是直接获取 geojson 数据并通过 geojson-vt 处理成类似矢量瓦片的格式,这样可以依旧使用 getTile 方法来获取主线程所需要的瓦片数据。

插一嘴:Mapbox 将 geojson 数据源这样处理后,一方面能够将 LOD 策略应用到 geojson 数据上,另一方面,底层的绘制逻辑只需要针对矢量瓦片这一种数据规范即可,而不需要同时考虑瓦片和 geojson 两种。

ImageSource

计算出一个瓦片编号,要满足 zoom 值足够大,并且这个瓦片的边界包含了 ImageSource 的坐标范围。只有在主线程正在请求这个瓦片时,loadTile() 会返回 true。(在使用这个 source 的图层被添加到地图中时,就已经去请求这个图片了)


当 vector 或者 geojson 类型的 source 请求的数据返回到主线程时,会通过 Tile#loadVectorData 来解析数据并存储到 buckets 中。

准备渲染

再回到 SourceCache 上,缺失的瓦片已经加载完成了,接下来就是:


Render loop

https://github.com/maplibre/maplibre-gl-js/blob/main/docs/diagrams/render-frame.plantuml.svg

当 _sourcesDirty 为 false 时,map#_render() 将会直接在主线程上渲染新的一帧:


分享这篇文章:

上一篇
用 GeoServer 发布适用于 Mapbox 的矢量切片
下一篇
学习笔记——Three.JS