mapbox地图自定义主题切换踩坑指南

3,429 阅读3分钟

1. 前言

因项目需要,需要对 mapbox 地图进行主题样式切换。通过官网查询,使用setStyle方法后,地图上原有的依赖源和图层全部被清空了。我惊呆 😮 了。

2. 原因

原来setStyle 方法更新时,不会保留原有的依赖源和图层,用最新的地图样式渲染。

3. 解决方案

第一版方案:

步骤:

  1. 记录地图中现有已经渲染的依赖源和图层;
  2. 监控地图样式切换;
  3. 地图样式切换;
  4. 当地图主题样式更新后,触发地图样式加载完成回调;
  5. 手动将原有的依赖源和图层再重新加载到地图实例中;

但是,你以为就这样结束了吗?,不,坑还有很多 😭。

踩坑 1:

地图样式加载事件styledata, 这瘪犊子玩意根本就不行,不能监控到样式切换加载完成,它的触发条件很多,不能对地图样式style 单独监控。没办法,我找了又找,终于找到一个未公开的事件style.load。可以监控到地图样式加载后的事件回调,实测可以用,版本mapbox-gl": "^2.2.0

但是style.load事件官方是不太建议用的,希望mapbox-gl的未来版本能够通过promiseasync方法简化这个事情。大家使用时,注意版本。

踩坑 2:

对同一个地图样式,通过setStyle方法重复设置,地图实例会挂掉,导致地图上的图层全清空。处理方法,需要对传入的地图样式进行缓存,判断当前传入的地图样式是否不等于上一次的地图样式。不相等就继续操作,相等就关闭返回,不继续操作。

所以,综合上面的问题,我们的最终版的解决方案如下:

第二版方案:

步骤:

  1. 进行闭包处理,对传入的地图样式进行缓存;
  2. 记录地图中现有已经渲染的依赖源和图层;
  3. 监控地图样式切换,绑定地图样式加载事件style.load
  4. 地图样式切换;
  5. 对比传入样式同上一次的样式,相同则函数返回,不同继续操作;
  6. 当地图主题样式更新后,触发地图样式加载完成回调;
  7. 手动将原有的依赖源和图层再重新加载到地图实例中;
  8. 卸载地图样式更新事件style.load,防止污染;

代码如下(基于typescript):

import deepEqual from 'deep-equal'

interface ISetMapStyle {
    map: mapboxgl.Map
    style: string | mapboxgl.Style
    layersList: string[]
    sourcesList: string[]
}

const setMapStyleFn = function (defaultStyle: string | mapboxgl.Style) {
    let prevStyle = defaultStyle
    return (props: ISetMapStyle) => {
        const { map, style, sourcesList, layersList } = props
        if (!map || !style || !layersList || !sourcesList) {
            return
        }

        if (deepEqual(prevStyle, style)) {
            return
        }
        prevStyle = style

        const mapStyle = map.getStyle();
        if (!mapStyle) {
            return
        }
        const layers = (mapStyle.layers || []).filter((layer) => layersList.includes(layer.id));
        const sources = Object.keys((mapStyle.sources || {})).filter((key) => {
            return sourcesList.includes(key)
        }).reduce((prev, result) => {
            prev[result] = (mapStyle.sources || {})[result];
            return prev;
        }, {});

        const handleStyle = () => {
            Object.keys(sources).forEach((key) => {
                const existing = map.getSource(key);
                if (!existing) {
                    map.addSource(key, sources[key]);
                }
            });
            layers.forEach((layer) => {
                const existing = map.getLayer(layer.id);
                if (!existing) {
                    map.addLayer(layer)
                }
            });
            map.off('style.load', handleStyle);
        }
        map.on('style.load', handleStyle);

        map.setStyle(style, { diff: true });
    }
}
export default setMapStyleFn

4. 使用

以下是伪代码示例,基于 react 的写法:

import setMapStyleFn from './index'

const map = mapServer; // 地图实例
const style = 'mapbox://styles/xxxxxxx' // 初始的地图样式
const style1 = 'mapbox://styles/xxxxxxx2' // 将要更新的地图样式
const setMapStyle = setMapStyleFn(style)

const onClick = () => {
    setMapStyle({
        map,
        style: style1,
        layersList: ['map-fill-layer', 'map-circle-layer'],
        sourcesList: ['map-fill-source', 'map-circle-source'],
    })
}

<button onClick={onClick}>地图样式更新</button>