Vue3 + Leaflet 实战:天地图与CGCS2000 WMTS服务的高效集成方案

最近在重构一个地理信息平台时,我选择了Vue3作为前端框架,搭配Leaflet实现地图功能。本以为这会是个简单的任务,没想到在集成天地图和CGCS2000坐标系的WMTS服务时,遇到了各种意想不到的"坑"。经过两周的摸索和调试,终于总结出一套稳定可靠的解决方案,现在分享给同样面临这些挑战的开发者们。

1. 环境准备与基础配置

在开始之前,我们需要确保项目环境正确配置。使用Vue3的Composition API可以让我们更灵活地组织地图相关的逻辑代码。

首先创建一个新的Vue3项目(如果尚未创建):

npm init vue@latest vue3-leaflet-demo
cd vue3-leaflet-demo
npm install

然后安装必要的依赖:

npm install leaflet leaflet.wmts @types/leaflet

在main.js或main.ts中引入Leaflet的CSS文件:

import 'leaflet/dist/leaflet.css'

提示:使用TypeScript可以获得更好的类型提示和开发体验,特别是在处理复杂的地图配置时。

创建一个基础的地图组件MapContainer.vue:

<template>
  <div id="map-container"></div>
</template>

<script setup>
import { onMounted } from 'vue'
import L from 'leaflet'

onMounted(() => {
  const map = L.map('map-container').setView([39.9042, 116.4074], 10)
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map)
})
</script>

<style scoped>
#map-container {
  height: 100vh;
  width: 100%;
}
</style>

这个基础配置可以验证Leaflet是否正常工作。接下来我们将逐步替换为天地图和CGCS2000 WMTS服务。

2. 自定义CGCS2000坐标系实现

Leaflet默认支持EPSG3857(Web墨卡托投影)和EPSG4326(WGS84地理坐标),但国内很多GIS数据使用CGCS2000坐标系(EPSG4490)。要让Leaflet支持这一坐标系,我们需要自定义CRS。

在Vue3组件中定义自定义坐标系:

// 在setup函数中定义
L.CRS.CustomEPSG4490 = L.extend({}, L.CRS.Earth, {
  code: 'EPSG:4490',
  projection: L.Projection.LonLat,
  transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5),
  scale: function(zoom) {
    return 256 * Math.pow(2, zoom - 1)
  }
})

这个自定义CRS解决了几个关键问题:

  • 坐标范围:将经度(-180,180)映射到(0,1),纬度(-90,90)映射到(0,1)
  • 缩放级别:保持与Leaflet默认一致的缩放比例
  • 瓦片坐标:正确处理瓦片索引计算

注意:如果发现地图显示偏移或错位,可能需要调整transformation参数。不同数据源可能需要微调这些值。

验证坐标系是否工作:

const map = L.map('map', {
  crs: L.CRS.CustomEPSG4490,
  center: [35, 105],
  zoom: 5
})

3. 天地图服务集成实战

天地图提供了多种服务类型,我们需要特别注意区分地理坐标系(c)和投影坐标系(w)的服务地址。

首先申请天地图开发者密钥(替换YOUR_TDT_KEY):

const tdtKey = 'YOUR_TDT_KEY'

创建天地图图层:

const createTianDiTuLayer = (type, crsType) => {
  const layerTypes = {
    img: 'img',
    cia: 'cia',
    vec: 'vec',
    cva: 'cva'
  }
  
  return L.tileLayer(
    `http://t0.tianditu.gov.cn/${layerTypes[type]}_${crsType}/wmts?` +
    `SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layerTypes[type]}` +
    `&STYLE=default&TILEMATRIXSET=${crsType}&FORMAT=tiles` +
    `&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tdtKey}`
  )
}

使用示例:

// 地理坐标系的影像底图
const imgLayer = createTianDiTuLayer('img', 'c')
// 地理坐标系的注记层
const ciaLayer = createTianDiTuLayer('cia', 'c')

// 添加到地图
const baseLayers = L.layerGroup([imgLayer, ciaLayer])
map.addLayer(baseLayers)

常见问题排查:

  1. 图层不显示 :检查控制台是否有CORS错误,可能需要配置代理
  2. 错位或偏移 :确认使用的是地理坐标系(c)而非投影坐标系(w)
  3. 模糊或加载慢 :检查缩放级别是否在有效范围内(通常0-18)

4. CGCS2000 WMTS服务加载技巧

对于CGCS2000坐标系的WMTS服务,我们需要使用leaflet.wmts插件。这里分享几个关键配置项和常见问题的解决方案。

首先安装插件:

npm install leaflet.wmts

基本使用方法:

import 'leaflet.wmts'

const loadWMTSLayer = (url, layerName) => {
  const matrixIds = Array.from({ length: 22 }, (_, i) => ({
    identifier: i.toString(),
    topLeftCorner: new L.LatLng(90, -180)
  }))
  
  return new L.TileLayer.WMTS(url, {
    layer: layerName,
    style: 'default',
    tilematrixSet: 'CGCS2000',
    format: 'image/png',
    crs: L.CRS.CustomEPSG4490,
    matrixIds: matrixIds
  })
}

高级配置选项:

  • matrixIds :必须与服务端的TileMatrix定义一致
  • tileSize :默认为256,可根据服务调整
  • opacity :控制图层透明度
  • zIndex :控制图层叠加顺序

性能优化技巧:

  1. 使用图层组管理多个WMTS图层
  2. 实现按需加载(视口内加载)
  3. 添加加载状态指示器
  4. 实现缓存策略减少重复请求

5. 完整实现与最佳实践

将上述各部分整合成一个完整的Vue3组件,这里展示关键部分:

<template>
  <div class="map-container">
    <div id="map"></div>
    <div class="control-panel">
      <button @click="toggleLayer('wmts')">切换WMTS</button>
      <button @click="toggleLayer('tdt')">切换天地图</button>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import L from 'leaflet'
import 'leaflet.wmts'
import 'leaflet/dist/leaflet.css'

const tdtKey = 'YOUR_TDT_KEY'
const mapInstance = ref(null)
const activeLayers = ref({})

// 初始化地图
onMounted(() => {
  initMap()
  setupLayers()
})

const initMap = () => {
  // 自定义坐标系
  L.CRS.CustomEPSG4490 = L.extend({}, L.CRS.Earth, {
    code: 'EPSG:4490',
    projection: L.Projection.LonLat,
    transformation: new L.Transformation(1/180, 1, -1/180, 0.5),
    scale: zoom => 256 * Math.pow(2, zoom - 1)
  })

  mapInstance.value = L.map('map', {
    crs: L.CRS.CustomEPSG4490,
    center: [35, 105],
    zoom: 5,
    maxZoom: 18,
    minZoom: 3
  })
}

const setupLayers = () => {
  // 天地图图层
  const tdtLayer = createTianDiTuLayer('img', 'c')
  // WMTS图层
  const wmtsLayer = loadWMTSLayer('YOUR_WMTS_URL', 'LAYER_NAME')
  
  // 默认显示天地图
  tdtLayer.addTo(mapInstance.value)
  
  activeLayers.value = {
    tdt: tdtLayer,
    wmts: wmtsLayer
  }
}

const toggleLayer = (layerName) => {
  Object.entries(activeLayers.value).forEach(([name, layer]) => {
    if (name === layerName) {
      if (mapInstance.value.hasLayer(layer)) {
        mapInstance.value.removeLayer(layer)
      } else {
        layer.addTo(mapInstance.value)
      }
    }
  })
}
</script>

<style scoped>
.map-container {
  position: relative;
  width: 100%;
  height: 100vh;
}

#map {
  width: 100%;
  height: 100%;
}

.control-panel {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1000;
  background: white;
  padding: 10px;
  border-radius: 4px;
  box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
</style>

在实际项目中,我还发现几个值得注意的点:

  1. 性能监控 :添加地图性能监控可以帮助发现潜在问题
map.on('load', () => console.timeEnd('map-load'))
map.on('zoomend', () => console.log('Current zoom:', map.getZoom()))
  1. 内存管理 :单页应用中,组件卸载时要清理地图
import { onUnmounted } from 'vue'

onUnmounted(() => {
  if (mapInstance.value) {
    mapInstance.value.remove()
    mapInstance.value = null
  }
})
  1. 响应式设计 :处理窗口大小变化
import { onMounted, onUnmounted } from 'vue'

const handleResize = () => {
  mapInstance.value.invalidateSize()
}

onMounted(() => {
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

这套方案已经在生产环境运行了半年多,支撑了日均上万次的地图访问,稳定性得到了验证。希望这些经验能帮助开发者们少走弯路,快速实现Vue3与Leaflet的高效集成。

更多推荐