OpenLayers导出功能:地图截图+GeoJSON导出完整方案,从Canvas合并到PDF生成

产品要求"用户可以导出当前地图视图为图片"。听起来简单,但OpenLayers的地图是由多个Canvas图层叠加的,直接截图只能拿到最顶层。我花了两天研究Canvas合并、跨域处理、PDF生成这些问题,最终实现了一个完整的导出方案。


一、地图截图:Canvas合并导出

1.1 核心原理

OpenLayers的地图由多个Canvas图层叠加,导出时需要:

  1. 等待所有图层渲染完成
  2. 创建一个新的Canvas
  3. 将所有图层的Canvas依次绘制到新Canvas上
  4. 导出为图片

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

相关推荐
苦瓜小生1 小时前
【前端】【力扣与手撕】十天带你刷完前端算法与手撕,全是最简单好记的最优解法!day4
前端·数据结构·算法·leetcode·面试
mayaairi1 小时前
Vue2 实战进阶:表单数据收集、指令系统与自定义指令全解析
前端·javascript·vue.js
计算机魔术师2 小时前
实测GPT-6 Astra后,我发现过去十年的专业门槛正在失效
前端
上单带刀不带妹2 小时前
小程序图片缓存问题:时间戳和版本号解决图片不更新
缓存·小程序·vue·uniapp
虎头金猫2 小时前
Pic Smaller 本地部署:压缩图片、转换格式,再搭建自己的 Web 工具
运维·前端·开源·aigc·ai编程·ai写作
hunterandroid2 小时前
Android ANR 排查实战:从线上告警到主线程卡点定位
android·前端
图扑软件2 小时前
下篇・换墨|主题/多语言/移动端,一套组件全覆盖
前端·javascript·ui·性能优化·数据可视化
程序员老赵2 小时前
Docker 部署 TeX Live:轻松搭建 LaTeX 论文排版编译平台
前端·docker·latex
Zhu7582 小时前
部署安全的pg数据库,搭配pgadmin实现web界面管理
前端·数据库·安全