产品要求"用户可以导出当前地图视图为图片"。听起来简单,但OpenLayers的地图是由多个Canvas图层叠加的,直接截图只能拿到最顶层。我花了两天研究Canvas合并、跨域处理、PDF生成这些问题,最终实现了一个完整的导出方案。
一、地图截图:Canvas合并导出
1.1 核心原理
OpenLayers的地图由多个Canvas图层叠加,导出时需要:
- 等待所有图层渲染完成
- 创建一个新的Canvas
- 将所有图层的Canvas依次绘制到新Canvas上
- 导出为图片
1.2 完整实现
javascript
const exportMapImage = () => {
map.value.once('rendercomplete', () => {
const mapCanvas = document.createElement('canvas')
const size = map.value.getSize()
mapCanvas.width = size[0]
mapCanvas.height = size[1]
const mapContext = mapCanvas.getContext('2d')
// 合并所有图层的Canvas
Array.prototype.forEach.call(
map.value.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer'),
(canvas) => {
if (canvas.width > 0) {
// 处理透明度
const opacity = canvas.parentNode.style.opacity || canvas.style.opacity
mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity)
// 处理CSS变换(高分屏适配)
const transform = canvas.style.transform
const matrix = transform.match(/^matrix\(([^\(]*)\)$/)
if (matrix) {
const values = matrix[1].split(',').map(Number)
CanvasRenderingContext2D.prototype.setTransform.apply(mapContext, values)
}
mapContext.drawImage(canvas, 0, 0)
}
}
)
mapContext.globalAlpha = 1
// 触发下载
const link = document.createElement('a')
link.download = `map-${Date.now()}.png`
link.href = mapCanvas.toDataURL('image/png')
link.click()
})
// 触发渲染
map.value.renderSync()
}
1.3 关键API说明
| API | 用途 |
|---|---|
map.once('rendercomplete', fn) |
等待渲染完成 |
map.renderSync() |
触发同步渲染 |
map.getSize() |
获取地图尺寸 |
canvas.toDataURL('image/png') |
Canvas转图片 |
二、导出为图片文件
2.1 PNG格式(默认)
javascript
const link = document.createElement('a')
link.download = 'map.png'
link.href = mapCanvas.toDataURL('image/png')
link.click()
2.2 JPEG格式(文件更小)
javascript
// quality: 0-1,越小文件越小,质量越低
link.href = mapCanvas.toDataURL('image/jpeg', 0.8)
link.download = 'map.jpg'
2.3 高清导出(2倍分辨率)
javascript
const exportHiRes = () => {
map.value.once('rendercomplete', () => {
const size = map.value.getSize()
const scale = 2 // 2倍分辨率
const mapCanvas = document.createElement('canvas')
mapCanvas.width = size[0] * scale
mapCanvas.height = size[1] * scale
const mapContext = mapCanvas.getContext('2d')
// 缩放Canvas上下文
mapContext.scale(scale, scale)
// ... 合并图层代码同上
const link = document.createElement('a')
link.download = 'map-hires.png'
link.href = mapCanvas.toDataURL('image/png')
link.click()
})
map.value.renderSync()
}
三、GeoJSON数据导出
3.1 导出绘制的数据
javascript
import {GeoJSON} from 'ol/format'
const exportGeoJSON = () => {
const format = new GeoJSON()
const features = drawSource.getFeatures()
const geojson = format.writeFeaturesObject(features, {
dataProjection: 'EPSG:3857',
featureProjection: 'EPSG:3857'
})
const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/json' })
const link = document.createElement('a')
link.download = `drawings-${Date.now()}.geojson`
link.href = URL.createObjectURL(blob)
link.click()
URL.revokeObjectURL(link.href) // 释放内存
}
3.2 Circle转Polygon
GeoJSON不支持Circle,导出前需转换:
javascript
const circleToPolygon = (circle, numPoints = 32) => {
const center = circle.getCenter()
const radius = circle.getRadius()
const coords = []
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * 2 * Math.PI
coords.push([
center[0] + radius * Math.cos(angle),
center[1] + radius * Math.sin(angle)
])
}
coords.push(coords[0].slice())
return new Polygon([coords])
}
3.3 复制到剪贴板
javascript
const copyToClipboard = async () => {
const geojson = format.writeFeaturesObject(features, { ... })
try {
await navigator.clipboard.writeText(JSON.stringify(geojson, null, 2))
alert('已复制到剪贴板')
} catch (err) {
alert('复制失败')
}
}
四、导出PDF(使用jsPDF)
4.1 安装依赖
bash
npm install jspdf
4.2 实现
javascript
import {jsPDF} from 'jspdf'
const exportPDF = () => {
map.value.once('rendercomplete', () => {
const mapCanvas = document.createElement('canvas')
const size = map.value.getSize()
mapCanvas.width = size[0]
mapCanvas.height = size[1]
const mapContext = mapCanvas.getContext('2d')
// 合并图层(同上)
Array.prototype.forEach.call(
map.value.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer'),
(canvas) => {
if (canvas.width > 0) {
mapContext.globalAlpha = canvas.parentNode.style.opacity || 1
mapContext.drawImage(canvas, 0, 0)
}
}
)
// 创建PDF
const imgData = mapCanvas.toDataURL('image/png')
const pdf = new jsPDF({
orientation: size[0] > size[1] ? 'landscape' : 'portrait',
unit: 'px',
format: [size[0], size[1]]
})
pdf.addImage(imgData, 'PNG', 0, 0, size[0], size[1])
pdf.save(`map-${Date.now()}.pdf`)
})
map.value.renderSync()
}
五、导出带标注的图片
5.1 问题
导出的图片没有UI控件(按钮、图例等)。
5.2 解决方案
将UI元素也绘制到Canvas上:
javascript
const exportWithUI = () => {
map.value.once('rendercomplete', () => {
const mapCanvas = document.createElement('canvas')
const size = map.value.getSize()
mapCanvas.width = size[0]
mapCanvas.height = size[1]
const ctx = mapCanvas.getContext('2d')
// 1. 绘制地图图层
Array.prototype.forEach.call(
map.value.getViewport().querySelectorAll('.ol-layer canvas, canvas.ol-layer'),
(canvas) => {
if (canvas.width > 0) {
ctx.globalAlpha = canvas.parentNode.style.opacity || 1
ctx.drawImage(canvas, 0, 0)
}
}
)
// 2. 绘制图例
ctx.globalAlpha = 1
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'
ctx.fillRect(size[0] - 120, size[1] - 180, 110, 170)
ctx.fillStyle = '#333'
ctx.font = 'bold 14px sans-serif'
ctx.fillText('图例', size[0] - 100, size[1] - 155)
// ... 绘制图例内容
// 3. 绘制标题
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'
ctx.fillRect(10, 10, 200, 40)
ctx.fillStyle = '#fff'
ctx.font = 'bold 16px sans-serif'
ctx.fillText('地图标题', 20, 38)
// 导出
const link = document.createElement('a')
link.download = 'map-with-ui.png'
link.href = mapCanvas.toDataURL('image/png')
link.click()
})
map.value.renderSync()
}
六、踩坑记录
坑1:导出图片空白
现象:导出的图片是空白的。
原因 :没有等渲染完成就导出,或者没有调用renderSync()。
解决:
javascript
map.value.once('rendercomplete', () => { ... })
map.value.renderSync() // 必须调用
坑2:导出图片有跨域问题
现象:瓦片图层在导出图片中不显示。
原因 :瓦片URL没有设置crossOrigin。
解决:
javascript
new TileLayer({
source: new XYZ({
url: '...',
crossOrigin: 'anonymous' // 必须设置
})
})
坑3:高分屏导出模糊
现象:Retina屏幕上导出的图片模糊。
原因:Canvas尺寸是逻辑像素,不是物理像素。
解决 :导出时乘以devicePixelRatio:
javascript
const dpr = window.devicePixelRatio || 1
mapCanvas.width = size[0] * dpr
mapCanvas.height = size[1] * dpr
mapContext.scale(dpr, dpr)
七、总结
| 功能 | 核心API | 关键点 |
|---|---|---|
| 地图截图 | rendercomplete + Canvas合并 |
必须等渲染完成 |
| PNG导出 | canvas.toDataURL('image/png') |
最常用 |
| JPEG导出 | canvas.toDataURL('image/jpeg', quality) |
文件更小 |
| 高清导出 | devicePixelRatio缩放 |
Retina屏幕 |
| GeoJSON导出 | GeoJSON.writeFeaturesObject() |
Circle先转Polygon |
| PDF导出 | jsPDF + Canvas转图片 |
需要额外依赖 |
更新日期 :2026年9月
调试版本:OpenLayers 10.9.0