1. 前言
因项目需要,需要对 mapbox 地图进行主题样式切换。通过官网查询,使用setStyle方法后,地图上原有的依赖源和图层全部被清空了。我惊呆 😮 了。
2. 原因
原来setStyle
方法更新时,不会保留原有的依赖源和图层,用最新的地图样式渲染。
3. 解决方案
第一版方案:
步骤:
- 记录地图中现有已经渲染的依赖源和图层;
- 监控地图样式切换;
- 地图样式切换;
- 当地图主题样式更新后,触发地图样式加载完成回调;
- 手动将原有的依赖源和图层再重新加载到地图实例中;
但是,你以为就这样结束了吗?,不,坑还有很多 😭。
踩坑 1:
地图样式加载事件styledata, 这瘪犊子玩意根本就不行,不能监控到样式切换加载完成,它的触发条件很多,不能对地图样式style
单独监控。没办法,我找了又找,终于找到一个未公开的事件style.load
。可以监控到地图样式加载后的事件回调,实测可以用,版本mapbox-gl": "^2.2.0
。
但是style.load
事件官方是不太建议用的,希望mapbox-gl
的未来版本能够通过promise
或async
方法简化这个事情。大家使用时,注意版本。
踩坑 2:
对同一个地图样式,通过setStyle
方法重复设置,地图实例会挂掉,导致地图上的图层全清空。处理方法,需要对传入的地图样式进行缓存,判断当前传入的地图样式是否不等于上一次的地图样式。不相等就继续操作,相等就关闭返回,不继续操作。
所以,综合上面的问题,我们的最终版的解决方案如下:
第二版方案:
步骤:
- 进行闭包处理,对传入的地图样式进行缓存;
- 记录地图中现有已经渲染的依赖源和图层;
- 监控地图样式切换,绑定地图样式加载事件
style.load
; - 地图样式切换;
- 对比传入样式同上一次的样式,相同则函数返回,不同继续操作;
- 当地图主题样式更新后,触发地图样式加载完成回调;
- 手动将原有的依赖源和图层再重新加载到地图实例中;
- 卸载地图样式更新事件
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>