1.内容看点
本文讲的是如何使用html2canvas 进行百度地图+覆盖物截图。
在阅读的过程中,可以接收到如下内容:
百度地图v3.0使用html2canvas截图的完整流程
百度地图v3.0版本和GL v1.0版本截图方式对比
注意:本文提到的百度地图v3.0、v3.0都是指JavaScript API v3.0,百度地图GL v1.0、GL v1.0都是指JavaScript API GL v1.0。
2.背景介绍
在现有的产品模型中,我们将城市按业务规则划分成多个区域,每个区域都是地图上的一个多边形覆盖物。比如下图示例场景(地图API v3.0版本实现),在北京市划分出三个区域:A、B、C。
现业务提出需求:
得到城市的区域划分概览图,比如北京市+区域A/B/C的地图截图。
针对上述需求,接下来我们使用html2canvas截图工具进行实践。
3.地图截图实现
3.1 html2canvas简介
html2canvas可以实现在浏览器中对网页或元素进行“截图”。有两种模式,
foreignObject和canvas。由于百度地图v3.0的地图是DOM节点渲染地图瓦片(图片)呈现,GL v1.0地图是canvas渲染呈现,因此选择使用更为通用的canvas模式。
API
html2canvas(element, options).then(
function(canvas) {
// TODO
}
);
使用DEMO
1)安装
npm install html2canvas
2)HTML
<div id="capture" style="padding: 10px; background: #f5da55">
<h4 style="color: #000; ">Hello world!</h4>
</div>
3)JavaScript
import html2canvas from 'html2canvas';
html2canvas(document.querySelector("#capture")).then(
canvas => {
// 返回Promise,得到截图内容的canvas对象
document.body.appendChild(canvas)
}
);
canvas对象
调用html2canvas函数后返回Promise实例,然后得到canvas对象,通过调用canvas对象的toDataURL 方法可以得到包含图片展示的data URI,我们可以将这个data URI:
放到img标签的src属性中,让其显示在网页中
放到a标签的href属性中,设置download属性,将图片下载到本地
html2canvas的使用简单介绍到这里,具体使用到的属性在后续文章使用中会有补充,更多的信息可以到html2anvas官网查看。
3.2 地图和覆盖物截取
3.2.1 地图模块代码实现
下面的代码使用百度地图JavaScript API v3.0 实现。
若是要用 JavaScript API GL v1.0,代码实现和v 3.0基本一致,只是需要改变百度地图的引入文件,同时将BMap变成BMapGL。
1、HTML
<div id='MAP_SHOT_ID' style={{height: '500px', width:' 500px'}}></div>
2、地图渲染代码
/**
@function 主程序
*/
// 初始化地图
const kemap = new window.BMap.Map('MAP_SHOT_ID')
// 设置中心点
kemap.centerAndZoom(new window.BMap.Point(116.4777778293003, 40.26420238319829), 9)
// 绘制北京的城市边界
addCityOverlay('北京市')
// 绘制多边形覆盖物
drawPolygon()
/**
@function 绘制城市边界,城市边界也是一个多边形覆盖物
*/
const addCityOverlay = (cityName, map) => {
new window.BMap.Boundary().get(cityName, (data) => {
const boundaries = data.boundaries
if (!boundaries.length) { return }
boundaries.forEach(item => {
if(!item) { return }
// 创建城市边界Point集合
const _points = item.split(';').map((poi) => (
new window.BMap.Point(poi.split(',')[0], poi.split(',')[1])))
// 创建多边形
const _polygon = new window.BMap.Polygon(_points, {
strokeColor: '#0984F9',
strokeWeight: 3,
strokeOpacity: 0.8,
fillOpacity: 0,
fillColor: ''
})
// 绘制城市边界
map.addOverlay(_polygon)
})
})
}
/**
@function 绘制覆盖物
@mapData [
[
"116.13088682610888,40.13210416329521",
"116.50803141141803,40.12504285535484",
"116.50803141141803,39.80297163977847",
"116.13548615031998,39.81361374229076",
"116.12714987518737,39.81389085819163"
],[
"116.12305744205753,40.53549608262856",
"116.50480135157775,40.53900525150335",
"116.51263073562912,40.25908124043422",
"116.13685541469079,40.26121043493562",
"116.13059936834568,40.26172404753967",
"116.13088682610888,40.266128494821274"
],
[
"116.64601113775065,40.54390289932435",
"116.83458343040522,40.54741162438473",
"117.0185563988487,40.54741162438473",
"117.02775504727087,40.26965184453788",
"116.64601113775065,40.266128494821274"
]
]
*/
const drawPolygon = (map) => {
mapData.forEach(item => {
const polygon = new window.BMap.Polygon(
item.map( i => (new window.BMap.Point(i.split(',')[0], i.split(',')[1]))),
{
strokeColor: '#0984F9',
fillColor: '#0984F9',
fillOpacity: 0.2,
strokeWeight: 2
}
)
map.addOverlay(polygon)
});
}
上面代码实现的就是背景介绍中的示例:北京市划分出三个区域。
接下来我们开始截图。
3.2.2 百度地图JavaScript API v3.0 截图
1、代码实现
设置截图宽高都为500,与地图大小一致,将得到的结果直接渲染到页面上(设置成横排):
html2canvas(document.querySelector('#MAP_SHOT_ID'), {
width: 500,
height: 500,
}).then((canvas) => {
document.body.append(canvas)
})
在chrome上执行上面的代码之后我们看到页面上现在呈现的内容如下(右图为截图):
2、问题分析和解决
可以看到生成的截图存在两个问题:
1)没有地图底图,没有左下角的logo
2)没有覆盖物:地图边界+区域
问题一:没有地图底图,没有左下角的logo
通过控制台Elements,我们可以看到v3.0版本的地图是使用多张地图瓦片组合而成,也就是说截图没有地图底图是因为图片渲染失败,经过分析渲染失败的原因图片跨域。
解决方案
html2canvas解决图片跨域问题有两种方式:
1)配置项useCORS:true
是否尝试使用CORS加载图像
执行代码得到下图:
我们可以看到地图的logo并没有成功截图。打开控制台可以看到copyright_logo.png不支持CORS跨域。
在图片的response header中我们也可以看到,地图瓦片支持CORS请求,但是logo等图片不支持,所以无法成功截取。
地图瓦片请求:
地图logo请求:
2)配置项allowTaint:true
是否允许图像污染画布
执行代码得到下图:
可以看到地图瓦片和百度logo都完整的截取出来了,但是配置allowTaint:true之后,canvas会被污染,将不能使用toDataURL导出data URI:
综合上述分析,因为我们的使用场景需要将canvas转化为图片下载,所以选择使用CORS模式,同时也能满足本次需求。
问题二:没有覆盖物(地图边界+区域)
地图底图的问题成功解决了,接下来我们要解决的就是没有覆盖物的问题。再次提醒,这里使用的是百度地图 JavaScript API v3.0。
首先我们通过控制台可以看到,地图上的多边形覆盖物(polygon)在DOM中是以SVG的形式展示的(其他的覆盖物可能是非SVG方式)。
通过html2canvas的资料阅读和源码分析得知,html2canvas支持SVG。
那我们跟踪代码看看是什么原因导致截图没有覆盖物。
1)源码分析
首先找到源码中渲染内容的代码:
1.1)文件位置:src/render/canvas/canvas-renderer.ts
1.2)函数名称是:renderNodeContent
1.3)npm安装的html2canvas,打包编译后的文件位置:
node_modules/html2canvas/html2canvas.js
下面截取了部分代码实现,如果元素是SVG,会调用renderReplacedElement方法将内容绘制到canvas上。
要找到这个源码也很简单,html2canvas的原理是逐层拆解DOM元素绘制到canvas上,所以全局搜索drawImage就可以定位到这个地方。
2)实践分析
下面我们进行实践分析(注意如果是在自己的项目中就改动node_modules下打包后的文件,如果是在html2canvas源码中实践就直接改动源文件)。
在renderReplacedElement方法的 if 判断内部的最后增加两行代码:
// 源码
renderReplacedElement(
container: ReplacedElementContainer,
curves: BoundCurves,
image: HTMLImageElement | HTMLCanvasElement) {
if (image && container.intrinsicWidth > 0 && container.intrinsicHeight > 0) {
const box = contentBox(container);
const path = calculatePaddingBoxPath(curves);
this.path(path);
this.ctx.save();
this.ctx.clip();
this.ctx.drawImage(
image,
0,
0,
container.intrinsicWidth,
container.intrinsicHeight,
box.left,
box.top,
box.width,
box.height);
this.ctx.restore();
// ADD-打印渲染元素的宽高等box信息
console.log('renderReplacedElement',
container.intrinsicWidth, container.intrinsicHeight, box )
// ADD-将要渲染的图片绘制在网页上
document.body.append(image)
}
}
执行代码之后我们得到如下信息:
从上面的图片我们可以看到覆盖物SVG宽高都是1500px,box.left是-500, box.top是-480,canvas的drawImage方法第六、七个参数解释如下:
也就是说在绘制覆盖物SVG的时候,在画布上绘制的起点坐标是( -500,-480 ),而我们设置的需要截取的画布尺寸是500,这显然偏离可视区域,也是覆盖物SVG无法正确截取的原因。
至于为什么覆盖物SVG元素的宽高会是1500px,以及存在偏移,这块是百度地图内部实现,暂不详细探究。
3)解决方法
html2canvas API暴露的一个方法属性onclone,可以修改进行渲染的DOM节点,同时不会影响原网页内容。
那么给html2canvas调用增加如下配置项来处理:将SVG元素的left和top设置为0,让渲染起点变为(0,0)(假设当前页面只有这个SVG,若实际网页中存在其他SVG元素,需要区分处理)。
onclone处理代码:
onclone: (target) => {
const svgElems = Array.from(target.getElementsByTagName('svg'))
for (let svg of svgElems) {
svg.style.left = 0
svg.style.top = 0
}
return target
}
最后得到结果如下,右侧是是截图内容,成功的将多边形覆盖物截取出来了。[手动撒花]
3、总结一下
百度地图v3.0 API实现截图的整体代码如下(实际的参数和结果处理按照实际情况自己调整):
html2canvas(document.querySelector('#MAP_SHOT_ID'), {
useCORS: true, // 允许跨域
width: 500,
height: 500,
onclone: (target) => { // 处理svg
const svgElems = Array.from(target.getElementsByTagName('svg'))
for (let svg of svgElems) {
svg.style.left = 0
svg.style.top = 0
}
return target
}
}).then((canvas) => {
document.body.append(canvas)
// TODO 下载图片-见后面下载图片
})
3.2.3 百度地图JavaScript API GL v1.0截图
GL v1.0的地图代码实现和v3.0版本的实现基本一致。但是如果要使用截图功能,需要在初始化地图时配置preserveDrawingBuffer:true,否则截图得到的是黑屏。
初始化代码如下所示:
// 初始化地图
const kemap = new window.BMapGL.Map('MAP_SHOT_ID', {preserveDrawingBuffer: true})
初始化之后渲染出来的地图如下:
我们可以明显看出和v3.0版本地图的差异:地图上的标注点层级高于多边形覆盖物,比如北京红色五角星,怀柔区。
从控制台查看元素,我们可以看到地图整个包括底图和覆盖物都在一个canvas中,因此在GL v1.0版本中使用html2canvas截图不需要额外设置图片跨域。
1、GL v1.0 截图方式
GL v1.0版本中截图除了可以使用html2canvas之外,地图实例自身也提供了截图功能,下面对两种方式进行分析对比。
1)地图实例方法getMapScreenshot()
从GL v1.0的类参考中我们可以看到,地图实例本身具备截图功能,得到的是base64数据:
1.1)代码实现
下面使用地图实例方法getMapScreenshot()截图并且把内容作为图片渲染到页面。注意代码执行时机,要在覆盖物渲染完成之后执行。
const dataURI = kemap.getMapScreenshot()
const img = document.createElement('img')
img.src = dataURI
document.body.append(img)
1.2)代码执行和分析
执行得到如下所示结果,右图为调用getMapScreenshot()方法得到的图片,截图完整的包含地图和覆盖物:
分析上面的图片,右侧的图片为截图结果,但是我们发现地图尺寸500 * 500,截图的尺寸是1000 * 1000,这是因为为了保证canvas绘制图片的清晰度,canvas在绘制过程中会将尺寸放大window.devicePixelRatio倍再使用scale缩小,而当前window.devicePixelRatio为2。
2)使用html2canvas
2.1)代码实现
html2canvas(document.querySelector('#MAP_SHOT_ID'), {
width: 500,
height: 500,
}).then((canvas) => {
document.body.append(canvas)
})
执行之后我们看到页面现在呈现的内容如下:
在本次场景中GL v1.0版本和v3.0版本的实现对比除了地图标注文字比覆盖物的层级高基本没有差别。
不过需要注意的是,GL v1.0版本在实现上和具备的功能上与v3.0版本有差异,比如3D效果等,那么在实际应用中需要按照实际的需求来进行选择。
4. 下载截图
通过上面的方法我们得到的包含截图信息的数据有下面两种:
1、canvas实例
2、base64
若是canvas实例,我们需要调用toDataURL方法得到包含图片展示的 data URI,之后操作同base64数据。
4.1 canvas实例产出dataURI
// 图片格式可以自己设置
const dataURI = canvas.toDataURL("image/jpeg", 1)
4.2 下载图片
图片内容的变量都定义为dataURI,下载图片的操作如下:创建下载链接,执行下载。
// 创建下载链接
const link = document.createElement("a")
link.style.display = "none"
// dataURI 是 GL v1.0 getMapScreenshot()得到的base64或者canvas实例toDataURL得到的data URI
link.href = dataURI
link.download = '区域地图.jpg'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
最后百度地图的两个版本的API的三种截图方式下载的图片如下:
5. 总结思考
1、百度地图v3.0 API 使用html2canvas对地图和覆盖物进行截图注意点
1)图片跨域问题:配置useCORS:true或者allowTaint:true解决图片跨域问题。
若配置useCORS:true,截图内容可以调用canvas的toDataURL方法,结合下载链接下载截图到本地,并且截取元素中包含的图片只有本身允许CORS访问才可以成功截取。
若配置allowTaint:true,所有图片均可绘制到画布上,但是画布被污染,无法使用toDataURL方法,从而无法下载截图到本地。
2)多边形覆盖物在百度地图中是以SVG的形式呈现,因为百度地图多边形覆盖物SVG本身实现的原因,导致html2canvas在截图时无法正确截取,设置onclone方法,将SVG的top和left设置为0。
2、百度地图GL v1.0 API可以使用两种方式进行截图
1)地图自带截图方法:getMapScreenshot()
2)使用html2canvas,因为GL v1.0的地图使用是canvas实现,因此不需要像v3.0版本一样特殊配置
3、场景&方案选择
因为本次的场景比较简单,是百度地图+多边形覆盖物。实际业务中地图上可能会有点、文本、图标、自定义信息等覆盖物,因此在实际开发中选取截图方式的时候,需要按照实际情况和需求进行对比选择最优。
本文内容就到这里,希望能给到你一些收获。
参考文档:
1、百度地图JavaScript API v3.0(
https://lbsyun.baidu.com/index.php?title=jspopular3.0)
2、百度地图JavaScript API GL v1.0(
https://lbsyun.baidu.com/index.php?title=jspopularGL)
3、html2canvas:(https://html2canvas.hertzen.com)