mapbox-gl-js可以接受GeoJSON数据,在前端动态地绘制GeoJSON数据。需要说明的是,mapbox-gl-js并不能直接渲染GeoJSON数据,而是通过geojson-vt这个库,在前端动态地将GeoJSON数据转换为矢量瓦片后渲染。至于为什么不支持直接渲染GeoJSON数据,我猜想是因为mapbox-gl-js对于点、线、面、符号、注记等要素的绘制规则都是基于矢量瓦片的,为GeoJSON另外实现一套绘制规则,不仅增大了实现成本,而且两套规则可能会出现冲突。

传统的矢量数据需要预先切片、存储、发布服务,并且当数据更新时又得重新切片,对于小项目来说有点大炮打蚊子的感觉。GeoJSON数据源的好处是接入容易、更新方便,并且市面上有很多工具可以将各种矢量数据转换为GeoJSON格式。

通常来说,GeoJSON数据源适合于少量的矢量数据,大批量的矢量数据最好还是预先切片。但多少数据算是”少量数据呢“?从经验上讲,应该将GeoJSON数据控制在20M以下。需要说明的是,之所以提出”20M“这个经验值,并不是因为mapbox-gl-js对于渲染超过20M的GeoJOSN数据有困难,而是综合考虑网络请求时间和解析数据的时间。实际上,mapbox-gl-js处理100M的GeoJSON数据都没有什么问题,但是把大量的GeoJSON数据整体请求过来所产生的耗时,会严重影响用户体验。所以对于大量的GeoJSON数据,最好预先切片,这样前端可以分块请求,减少网络请求时间和解析数据的时间。

虽然使用GeoJSON大文件作为数据源不是最佳选择,但有些情况下我们不得不使用GeoJSON作为数据源,例如数据是由第三方通过API提供的、数据是实时更新的。当接入大GeoJSON数据时,我们可以通过以下几种优化策略,提高地图绘制的效率。

使用cluster选项

如果需要接入的数据是点数据,并且十分密集,这时应该对点进行聚合。特别密集的点在小缩放级别会重叠在一起,造成注记压盖叠加,可视化效果不好。使用聚合后,小的缩放级别显示聚合数据,大的缩放级别显示原始数据,兼顾了综合和细节。并且,聚合减少了单张瓦片的数据量,使得引擎绘制更快,交互更流畅。

mapbox-gl-js是通过supercluster这个库对GeoJSON数据进行聚合。其原理是两个点的显示距离小于聚合半径时,聚合为一个点要素,这个点会有一个point_count字段记录聚合的点的数目。需要注意的是,并不是每个层级上所有的点都被聚合,有些孤立的点会保留原位。那么如何区分聚合的和未聚合的点要素呢?可以使用有没有point_cluster进行判断,即使用"filter": ["has", "point_count"]过滤出聚合的点要素,使用"filter": ["!has", "point_count"]过滤出未聚合的点要素。

mapbox-gl-js通过clusterclusterRadiusclusterMaxZoom三个参数控制点聚合的效果,并且是要在source里面配置。从逻辑上讲,这三个是控制聚合的效果的,应该放到layer上更符合直觉。然而,mapbox-gl-js的聚合过程并不是在渲染层实现的,而是在数据层实现的,即在动态切片的时候对数据做了聚合操作。所以,从实现角度来讲,将聚合控制参数放在source是合情合理的。以下是这三个参数的说明:

  • cluster:设为true开启GeoJSON数据聚合,只对点数据有效;
  • clusterRaius:聚合半径,默认为50像素。当你发现有很多点没有被聚合时,可以适当调大这个参数;
  • clusterMaxZoom:聚合的最大层级,大于这个层级的数据不再聚合,默认值是maxzoom - 1

减小buffer

矢量瓦片一般会向边界外围扩张一定距离,作为缓冲区,以免边界上的接边要素出现断开的现象。缓冲区越大,出现断裂的可能性越小,但同时导致每张瓦片的尺寸变大,渲染的效率也更低。可以适当减小缓冲区的大小,减小瓦片的额体积,提高渲染效率。

buffer的默认值是128像素,能够应付大部分情况。当GeoJSON是点数据,并且用来渲染点要素的符号和注记文本宽度比较小时,可以尝试将buffer设置为0或者稍大的值。

减小maxzoom

maxzoom用来控制将GeoJSON数据切割成矢量瓦片的最大级别,默认值是18级。减小maxzoom,可以加快切片的速度。一般来说,切割到12级,可以保证一定的精度和速度,满足大部分应用需求。

对数据进行minify

对GeoJSON数据进行minify,即移除JSON中多余的空格、换行符、注释,以及减少坐标点的小数位,可以减小GeoJSON的体积,减小加载时间。

使用URL加载数据

使用GeoJSON作为数据源,既可以将GeoJSON数据内联到样式文件中,也可以通过URL引用。从直觉上讲,使用内联的GeoJSON不需要网络请求,应该比URL方式更快,然而并不是这样的。使用内联的方式加载大量GeoJSON数据,有以下几个弊端:

  1. 使样式文件变大,增加了解析的时间;
  2. 内联的GeoJSON数据解析后无法释放,将会一直占用内存。

因此,对于大GeoJSON数据,尽量以URL的方式加载,可以减少客户端的内存占用。

渲染时增大minzoom

在配置GeoJSON渲染图层时,默认的minzoom是0,可以适当增大minzoomgeojson-vt在动态切片时,默认小于5级的瓦片是按需生成,大于或等于5级是预先生成。所以,但渲染图层的minzoom大于等于5时,小层级的瓦片不会生成,减少了计算负担。另外,增大minzoom,可以在小层级不需要加载、解析、渲染瓦片,所以渲染更为流畅。

渲染时允许符号重叠

在配置GeoJSON渲染图层时,可以设置允许注记符号重叠,即设置icon-allow-overlap: true,提高渲染的效率。

mapbox-gl-js在渲染注记符号和文本时,会有一个碰撞检测的过程,即当两个注记有重叠,则隐藏一个。常规条件下,这个碰撞检测很快,耗时可以忽略不计。但是,如果数据中的点非常密集,这部分耗时还是比较客观的。所以,设置icon-allow-overlap: true关闭这个检测过程,可以提高渲染效率。

当然,可以进一步设置允许注记文本也可以重叠,即设置text-allow-overlap: true,继续提高渲染效率。然而,从用户的角度来说,地图上出现符号重叠可以忍受,文本重叠就比较难看了。所以,一般不推荐设置允许注记文本重叠。

对数据进行切分

如果GeoJSON数据实在很大,可以将GeoJSON数据切分为几块,分别加载。比如,原来是一个GeoJSON大文件,现在把它切分为两个比较小的GeoJSON文件,可以提高处理的效率,原因在于:

  1. 切分的数据可以并行请求。例如,请求两个5M的数据,比单独请求一个10M的数据要快;
  2. geojson-vt处理小文件比处理大文件快。

预先切片

如果GeoJSON数据太大了,比如100M,以上的种种优化方法都不可行。GeoJSON没办法增量加载,必须将完整的GeoJSON数据请求过来,才能开始处理。大的GeoJSON数据,光网络请求就会占用大量时间,所以必须预先切片,分块加载。

预先切片有诸多好处,如减少网络请求时间、渲染效率高。但是,预先切片需要工具切片、瓦片服务器发布服务,而且数据更新有点麻烦。到底切不切片,是动态切片还是预先切片,需要根据数据的情况来确定。

你可以将数据上传到Mapbox Studio进行自动切片,也可以使用开源工具tippecanoe对GeoJSON数据自行切片。

参考链接

  1. https://blog.mapbox.com/rendering-big-geodata-on-the-fly-with-geojson-vt-4e4d2a5dd1f2
  2. https://blog.mapbox.com/clustering-millions-of-points-on-a-map-with-supercluster-272046ec5c97
  3. https://gist.github.com/ryanbaumann/2d5c851aebf46e4ef5702ee29ead6bdb