乐趣区

自定义mapbox插件-地图快照下载JS

效果预览

mapbox 是一个非常好用的开源地图引擎,他支持得平台有 android,ios,js,rn 等等,功能多样,但是对于地图插件开发这一块,没找到具体的实施文档。因此本文以 js 为例,来把开发 mapbox 插件这一过程记录下来。

mapbox

var map = new mapboxgl.Map({
    container: "map", // container id
    style: "mapbox://styles/mapbox/streets-v11", // stylesheet location
    center: [-74.50, 40], // starting position [lng, lat]
    zoom: 9, // starting zoom
    preserveDrawingBuffer: true // 这个需要开启,才能获取正确的 base64
  });

在开发之前先简述下 mapbox 的地图。在显示一张地图时,有两个属性是必须的,一个就是 container,地图的容器,接受一个 dom 的 id,另一个就是 style,地图实际渲染所需的资源配置都在这里,mapbox 是支持室内外地图的,也就是在 style 的 source 属性中去分别加载 indoor,outdoor 的资源(可以是瓦片,也可以是 geojson),有了这两个属性,就可以将地图显示出来了,其余属性不过多介绍。

mapbox 插件

mapbox 官方提供了很多插件,如线面绘制,地图比较等等。本次我开发的插件功能很简单,下载地图的快照,即将当前地图显示导出图片。mapbox 渲染完毕是一个 canvas 标签,而 canvas 可以直接转成图片的 base64 资源,然后转成文件资源去进行下载。

本文重点放在开发一款 mapbox 插件,而非下载功能本身,所以具体下载流程在接下来的插件开发中插入。

插件开发流程

因为官方没有提供开发插件的文档(没找到),因此从 0 到 1 这样一步一步来。

map.addControl(new mapboxgl.NavigationControl()); // 官方代码
// 插件的类
class Map2img {constructor() {}}
  map.addControl(new Map2img ()); 

上述代码为 mapbox 的一个示例,mapbox 提供了 addControl 这个 api 用于将插件引入地图,在初始化好一个类(Map2img),以同样的方式引入 map,此时出现报错:

由此可知,一个可供 map 使用的插件类至少需要 2 个方法,onAdd,onRemove。为了更详细的了解这两个方法的作用,直接去 mapbox-gl-js 里面搜索 addControl。
mapbox-gl 部分代码:

 addControl(control: IControl, position?: ControlPosition) {if (position === undefined && control.getDefaultPosition) {position = control.getDefaultPosition();
        }
        if (position === undefined) {position = 'top-right';}
        if (!control || !control.onAdd) {
            return this.fire(new ErrorEvent(new Error('Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.')));
        }
        const controlElement = control.onAdd(this);
        this._controls.push(control);

        const positionContainer = this._controlPositions[position];
        if (position.indexOf('bottom') !== -1) {positionContainer.insertBefore(controlElement, positionContainer.firstChild);
        } else {positionContainer.appendChild(controlElement);
        }
        return this;
    }

从源码中不难看出,在把插件加入 map 之后,会触发插件(control)上的 onAdd 方法,这个方法返回一个 dom 元素,元素被插入到 mapbox 的控制器(插件中),相当于把插件放入一个插槽。
因此,首先将类增加一个 onAdd 方法,并返回一个 dom 元素,然后让他显示至右上角(top-right)。
Map2img.js

class Map2img {constructor(html) {this._html = html; // 初始化接收要显示的 html}
       onAdd(map) {
    this._map = map;
    const el = document.createElement("div");
    el.innerHTML = this._html
    this.bindEvent(el) // 添加点击事件
    return el // 返回这个传入的 html
  }
   onRemove(map) {this.container.parentNode.removeChild(this.container);
    this._map = null;
    return this
  }
  }

index.html

  let eltemp = `<div style="width: 40px;height: 20px;background-color: gray;text-align: center" > 插件 </div>`
  var downloadCtrl=new Map2img(eltemp)
  map.addControl(downloadCtrl, 'top-right');

这样,就完成了插件的第一步,显示嵌入地图的问题。继续在加入的 dom 节点上增加点击监听事件,再点击之后通过在 onAdd 方法中获取的地图上下文,进而获取到地图的 canvas

 bindEvent(el) {el.addEventListener("click", () => {const base64 = this._map.getCanvas().toDataURL()
      this.downloadFile(this._map.getStyle().name, base64)
    })
  }

做到这一步后,发现并没有按照预想的结果,点击后触发相关事件。在这里卡了很久,没有找到原因,尝试过更改 dom 的 z -index 等等,更改事件监听方式等等,均没有触发点击效果。最终去翻阅官方的插件代码,发现官方的插件中,对于引入的 dom,添加了一个 mapboxgl-ctrl 的样式,去 mapbox-gl 中搜索这个样式后,发现一个关键属性。

在这个 css 中,有一个控制很关键。pointer-events 当这个属性为 none 时会阻止点击事件的触发(还有很多其他控制,不展开叙述),由此打开浏览器调试发现,果然插入 dom 的父级把这个属性置为 none。

在加入这个样式之后,引入的插件成功的触发了点击方法。之后通过插件本身拿到的地图上下文,开始下载。

downloadFile(fileName, content) {let aLink = document.createElement("a");
    let blob = this.base64ToBlob(content); //new Blob([content]);
    let evt = document.createEvent("HTMLEvents");
    evt.initEvent("click", true, true);//initEvent 不加后两个参数在 FF 下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
    aLink.download = fileName;
    aLink.href = URL.createObjectURL(blob);
    aLink.dispatchEvent(new MouseEvent("click", {bubbles: true, cancelable: true, view: window}));
  }

  base64ToBlob(code) {let parts = code.split(";base64,");
    let contentType = parts[0].split(":")[1];
    let raw = window.atob(parts[1]);
    let rawLength = raw.length;

    let uInt8Array = new Uint8Array(rawLength);

    for (let i = 0; i < rawLength; ++i) {uInt8Array[i] = raw.charCodeAt(i);
    }
    return new Blob([uInt8Array], {type: contentType});
  }

至此,一个下载地图快照的插件就完成了。

PS:

如果是室内外地图,有分楼层下载地图快照且不希望下载中去切换楼层,影响当前显示 的需求的话,我目前的做法是通过绝对定位,初始化一个新的地图在下面,然后用这个新的地图去切换楼层,然后将新地图的快照截取出来,因为地图下载只能是当前 camera(视口)的图象。还有一点需要注意的是,如果不是手动触发,而是在地图 load 时就下载地图的话,需要主动延迟适当的时间,因为地图 onload 的方法不包含地图字体的显示加载,即区域名称,所以要有必要的延迟,以上是我目前解决问题的思路,有更好的方法欢迎交流分享!

项目地址:https://github.com/jiwenjiang…

退出移动版