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

相关推荐
原则猫6 小时前
UDP 到底怎么实现可靠的
前端
Java后端的Ai之路6 小时前
14、ReactJS - 路由(Router)
前端·react.js·状态模式
裕晟资质规划7 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全
ShineWinsu7 小时前
对于JavaScript:JavaScript基础语法的解析
开发语言·前端·javascript·面试·web·基础语法·循环
月夏7 小时前
Vue3 转 React:18 项对照表 + 5 个必踩的坑(附 7 天上手路线)
前端
广州华水科技8 小时前
单北斗GNSS位移监测系统的应用与维护解析
前端
梦曦i8 小时前
uni-storage 0.1.0 发布:TTL过期管理+多实例隔离
前端·storage·uni-app x
猫头_9 小时前
AI 流式传输工程指南:模型吐出半截 JSON,前端如何边收边渲染?
前端·javascript·llm
console.log('npc')10 小时前
第 1 章 项目概述与环境搭建
前端·人工智能·大模型
定栓10 小时前
Webpack迁到Vite第七节(二):全家桶移除 — Babel 全家桶深度分析
前端·webpack·node.js