Skip to content

天地图使用经验总结

一、项目背景与概述

天地图作为国家地理信息公共服务平台,在物联网监控、车辆跟踪、电子围栏等场景中发挥着重要作用。本项目基于 Vue 3 + TypeScript 技术栈,深度封装了天地图组件,实现了车辆实时监控、轨迹回放、电子围栏绘制等核心功能。

二、天地图 API 导入与初始化

2.1 API 脚本加载策略

天地图 API 的加载需要考虑多个依赖库的顺序加载:

typescript
// 动态加载天地图核心API
const loadTianDiTuAPI = () => {
  const script = document.createElement('script')
  script.type = 'text/javascript'
  script.src = 'https://api.tianditu.gov.cn/api?v=4.0&tk=YOUR_API_KEY'
  return new Promise((resolve, reject) => {
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

// 依次加载依赖库
const loadDependencies = async () => {
  await loadTianDiTuAPI()
  await loadD3Library() // D3.js用于高级图形渲染
  await loadD3SvgOverlay() // SVG覆盖层支持
  await loadCarTrack() // 车辆轨迹动画库
}

// 如果使用的是vue3,可以直接在onMounted中加载
onMounted(async () => {
  // 动态加载天地图API
  const script = document.createElement('script')
  script.type = 'text/javascript'
  // 注意:实际使用时需要将YOUR_API_KEY替换为真实的天地图API密钥
  script.src =
    'https://api.tianditu.gov.cn/api?v=4.0&tk=6cbce7b12b1ba2d67a70dbaa3b7bb307'
  script.onload = () => {
    // 加载天地图API成功后,加载D3.js
    const d3Script = document.createElement('script')
    d3Script.type = 'text/javascript'
    d3Script.src = 'https://cdn.bootcdn.net/ajax/libs/d3/3.5.17/d3.min.js'
    d3Script.onload = () => {
      // 加载D3.js成功后,加载D3SvgOverlay.js
      const d3OverlayScript = document.createElement('script')
      d3OverlayScript.type = 'text/javascript'
      d3OverlayScript.src =
        'http://lbs.tianditu.gov.cn/api/js4.0/opensource/openlibrary/D3SvgOverlay.js'
      d3OverlayScript.onload = () => {
        // 加载D3SvgOverlay.js成功后,加载CarTrack.js
        const carTrackScript = document.createElement('script')
        carTrackScript.type = 'text/javascript'
        carTrackScript.src =
          'http://lbs.tianditu.gov.cn/api/js4.0/opensource/openlibrary/CarTrack.js'
        carTrackScript.onload = () => {
          // 所有脚本加载完成后初始化地图
          initMap()
        }
        carTrackScript.onerror = () => {
          console.error('CarTrack.js加载失败')
          // 即使CarTrack加载失败,也尝试初始化地图
          initMap()
        }
        document.head.appendChild(carTrackScript)
      }
      d3OverlayScript.onerror = () => {
        console.error('D3SvgOverlay.js加载失败')
        // 即使D3SvgOverlay加载失败,也尝试初始化地图
        setTimeout(() => {
          initMap()
        }, 300)
      }
      document.head.appendChild(d3OverlayScript)
    }
    d3Script.onerror = () => {
      console.error('D3.js加载失败')
      // 即使D3加载失败,也尝试初始化地图
      initMap()
    }
    document.head.appendChild(d3Script)
  }

  script.onerror = () => {
    console.error('天地图API加载失败')
  }

  document.head.appendChild(script)
})

关键难点:

  • 必须严格按照依赖顺序加载,否则会出现 API 未定义错误
  • 需要处理网络异常时的降级方案
  • API 密钥管理要做好安全防护

2.2 地图实例初始化

typescript
const initMap = (): void => {
  if (window.T) {
    const T = window.T

    // 创建地图实例,使用Web墨卡托投影
    const map = new T.Map('mapDiv', {
      projection: 'EPSG:900913'
    })

    // 设置初始视野(南京市中心)
    map.centerAndZoom(new T.LngLat(118.796877, 32.060255), 12)

    // 添加控件
    addMapControls(map, T)

    // 初始化搜索功能
    initLocalSearch(map, T)

    mapInstance.value = map
    emit('map-ready') // 通知父组件地图就绪
  }
}

三、TypeScript 类型定义与接口设计

3.1 完整的类型系统

为了提供良好的开发体验,项目定义了完整的 TypeScript 接口:

typescript
// 核心API接口
export interface TianMapAPI {
  Map: new (container: string, options: any) => TMap
  LngLat: new (lng: number, lat: number) => TLngLat
  Marker: new (lnglat: TLngLat, options?: any) => TMarker
  Polygon: new (points: TLngLat[], opts?: any) => TPolygon
  CarTrack: new (container: any, config: any) => any
  // ... 更多API接口定义
}

// 地图实例接口
export interface TMap {
  centerAndZoom: (center: TLngLat, zoom: number) => void
  addOverLay: (overlay: TOverlay) => void
  removeOverLay: (overlay: TOverlay) => void
  addEventListener: (event: string, handler: Function) => void
  // ... 更多方法定义
}

设计亮点:

  • 全面覆盖天地图 API,提供智能提示
  • 接口继承关系清晰,便于扩展
  • 支持事件系统的类型约束

四、车辆轨迹功能实现

4.1 轨迹数据结构设计

typescript
interface TrackPoint {
  lng: number // 经度
  lat: number // 纬度
  timestamp?: number // 时间戳(可选)
}

const vehicleTrackData = ref<TrackPoint[]>([])

4.2 轨迹动画播放核心实现

typescript
const playTrackAnimation = (speed = 1): void => {
  if (!mapInstance.value || !window.T || vehicleTrackData.value.length === 0) {
    console.error('地图实例未初始化或轨迹数据为空')
    return
  }

  // 停止现有动画
  if (carTrack.value) {
    carTrack.value.stop()
    carTrack.value = null
  }

  // 准备轨迹数据
  const trackData = vehicleTrackData.value.map(
    (point) => new window.T.LngLat(point.lng, point.lat)
  )

  // 配置CarTrack参数
  const carTrackOptions = {
    interval: Math.max(50, Math.floor(800 / speed)), // 播放间隔
    speed: Math.max(1, speed * 10), // 速度系数
    dynamicLine: true, // 显示动态轨迹线
    Datas: trackData,
    carstyle: {
      display: true,
      iconUrl: 'http://lbs.tianditu.gov.cn/images/openlibrary/car.png',
      width: 42,
      height: 32,
      direction: true // 车辆图标根据行驶方向旋转
    },
    polylinestyle: {
      color: '#409EFF',
      width: 4,
      opacity: 0.8
    }
  }

  // 创建并启动轨迹动画
  carTrack.value = new window.T.CarTrack(mapInstance.value, carTrackOptions)
  carTrack.value.start()

  // 启动进度追踪
  startProgressTracking(speed)
}

4.3 轨迹播放控制难点

核心难点:

  1. 天地图 CarTrack 不支持暂停/恢复:只能通过重新播放实现
  2. 进度追踪:需要自行实现定时器追踪播放进度
  3. 播放完成检测:通过轮询方式检测动画是否结束
typescript
// 进度追踪解决方案
const startProgressTracking = (speed = 1): void => {
  const baseInterval = 800
  const pointInterval = Math.max(50, Math.floor(baseInterval / speed))

  const updateProgress = () => {
    if (isPlayingTrack.value && carTrack.value) {
      const elapsedTime = Date.now() - (carTrack.value.startTime || Date.now())
      const estimatedIndex = Math.floor(elapsedTime / pointInterval)

      if (estimatedIndex < vehicleTrackData.value.length) {
        currentTrackIndex.value = estimatedIndex
      }

      if (isPlayingTrack.value) {
        trackTimer.value = window.setTimeout(updateProgress, 100)
      }
    }
  }

  if (carTrack.value) {
    carTrack.value.startTime = Date.now()
  }
  trackTimer.value = window.setTimeout(updateProgress, 100)
}

五、电子围栏绘制功能

5.1 交互式围栏绘制

电子围栏的绘制采用点击式交互,支持实时预览:

typescript
const drawPolygon = (): void => {
  if (!mapInstance.value || !window.T) return

  // 初始化绘制状态
  isDrawingFence.value = true
  isDrawing.value = true
  fencePoints.value = []

  // 绑定地图事件
  mapInstance.value.addEventListener('click', handleMapClick)
  mapInstance.value.addEventListener('mousemove', handleMouseMove)

  console.log('开始绘制电子围栏,请在地图上点击添加围栏点')
}

const handleMapClick = (e: any): void => {
  if (!isDrawingFence.value || !mapInstance.value || !window.T) return

  const clickPoint = new window.T.LngLat(e.lnglat.lng, e.lnglat.lat)

  // 检查是否点击在起始点附近(闭合围栏)
  if (fencePoints.value.length >= 3) {
    const firstPoint = fencePoints.value[0]
    const distance = calculateDistance(clickPoint, firstPoint)

    if (distance < 0.001) {
      // 约100米范围内
      completeFence()
      return
    }
  }

  // 添加新点
  fencePoints.value.push(clickPoint)
  addPointMarker(clickPoint)
  updateTempLines()
}

5.2 实时线条预览

typescript
const handleMouseMove = (e: any): void => {
  if (!isDrawingFence.value || !mapInstance.value || !window.T) return
  if (fencePoints.value.length === 0) return

  const mousePoint = new window.T.LngLat(e.lnglat.lng, e.lnglat.lat)

  // 移除旧的鼠标跟随线
  if (mouseFollowLine.value) {
    mapInstance.value.removeOverLay(mouseFollowLine.value)
  }

  // 创建从最后一个点到鼠标位置的线
  const lastPoint = fencePoints.value[fencePoints.value.length - 1]
  mouseFollowLine.value = new window.T.Polyline([lastPoint, mousePoint], {
    color: '#409eff',
    weight: 2,
    opacity: 0.6,
    lineStyle: 'dashed' // 虚线样式
  })

  mapInstance.value.addOverLay(mouseFollowLine.value)
}

5.3 围栏编辑功能

支持拖拽节点重新编辑围栏形状:

typescript
const startEditFence = (): void => {
  if (!fencePolygon.value) return

  isEditingFence.value = true

  // 移除多边形,显示可编辑的点
  mapInstance.value?.removeOverLay(fencePolygon.value)
  fencePolygon.value = null

  // 为每个点添加可拖拽标记
  fencePoints.value.forEach((point, index) => {
    addDraggablePointMarker(point, index)
  })

  redrawFenceLines()
}

const addDraggablePointMarker = (point: TLngLat, index: number): void => {
  const icon = new window.T.Icon({
    iconUrl: 'custom-point-icon.png',
    iconSize: new window.T.Point(10, 10),
    iconAnchor: new window.T.Point(5, 5)
  })

  const marker = new window.T.Marker(point, { icon })
  mapInstance.value?.addOverLay(marker)
  pointMarkers.value.push(marker)

  // 设置拖拽样式
  const markerDiv = marker.getElement()
  if (markerDiv) {
    markerDiv.style.backgroundColor = '#409eff'
    markerDiv.style.borderRadius = '50%'
    markerDiv.style.cursor = 'move'
  }
}

六、地理位置搜索功能

6.1 智能搜索实现

typescript
const initLocalSearch = (map: TMap, T: any): void => {
  const searchConfig = {
    pageCapacity: 10,
    onSearchComplete: handleSearchComplete
    // 全国范围搜索,不指定行政区域
  }

  localSearch.value = new T.LocalSearch(map, searchConfig)
}

const handleSearchInput = (): void => {
  if (searchKeyword.value.trim()) {
    showResults.value = true
    selectedIndex.value = -1

    // 防抖处理,避免频繁请求
    if (window.searchTimeout) {
      clearTimeout(window.searchTimeout)
    }

    window.searchTimeout = setTimeout(() => {
      if (localSearch.value && searchKeyword.value.trim()) {
        localSearch.value.search(searchKeyword.value, 4)
      }
    }, 300)
  } else {
    searchResults.value = []
    showResults.value = false
  }
}

6.2 键盘导航支持

typescript
// 支持上下键选择,回车确认
const handleKeyDown = (): void => {
  if (searchResults.value.length > 0 && showResults.value) {
    if (selectedIndex.value < searchResults.value.length - 1) {
      selectedIndex.value++
    } else {
      selectedIndex.value = 0 // 循环到第一项
    }

    // 更新搜索框值并滚动到可视区域
    searchKeyword.value = searchResults.value[selectedIndex.value].name
    scrollToSelectedItem()
  }
}

const handleEnterKey = (): void => {
  if (selectedIndex.value >= 0 && searchResults.value[selectedIndex.value]) {
    handleSelectResult(searchResults.value[selectedIndex.value])
  } else if (searchKeyword.value.trim()) {
    handleSearch()
  }
}

七、组件架构设计

7.1 主组件结构

typescript
// TianMap/index.ts - 组件入口
import TianMap from './src/index.vue'
export { TianMap }

// 主组件暴露的方法
const exposeMap = {
  setCarList, // 设置车辆列表
  completeFence, // 完成围栏绘制
  playTrackAnimation, // 播放轨迹动画
  stopTrackAnimation, // 停止轨迹
  clearMap // 清除地图内容
}

defineExpose(exposeMap)

7.2 子组件设计

vue
<!-- FenceForm.vue - 围栏属性配置表单 -->
<template>
  <Dialog :title="dialogTitle" v-model="dialogVisible">
    <el-form :model="formData" :rules="formRules">
      <el-form-item label="围栏名称" prop="name">
        <el-input v-model="formData.name" />
      </el-form-item>
      <el-form-item label="用途类型" prop="useType">
        <el-select v-model="formData.useType">
          <el-option label="危废运输" value="1" />
          <el-option label="处置监管" value="2" />
        </el-select>
      </el-form-item>
      <!-- 更多表单项... -->
    </el-form>
  </Dialog>
</template>

7.3 状态管理

typescript
// 响应式状态管理
const mapState = reactive({
  mapInstance: null,
  isDrawing: false,
  isPlaying: false,
  vehicleList: [],
  fenceList: [],
  searchResults: []
})

// 计算属性
const canCompleteDrawing = computed(
  () => mapState.isDrawing && fencePoints.value.length >= 3
)

const playButtonText = computed(() => (mapState.isPlaying ? '暂停' : '播放'))

八、性能优化策略

8.1 大量标记点优化

typescript
// 标记点聚合显示
const addVehicleMarkers = (vehicles: VehicleInfo[]) => {
  // 当车辆数量过多时使用聚合
  if (vehicles.length > 100) {
    enableMarkerClustering(vehicles)
  } else {
    vehicles.forEach((vehicle) => addSingleMarker(vehicle))
  }
}

// 视野范围内渲染
const addMarkersInViewport = () => {
  const bounds = mapInstance.value?.getBounds()
  const visibleVehicles = vehicleList.value.filter((vehicle) =>
    isPointInBounds(vehicle, bounds)
  )

  visibleVehicles.forEach((vehicle) => addVehicleMarker(vehicle))
}

8.2 内存管理

typescript
// 清理资源
const cleanupResources = () => {
  // 停止定时器
  if (trackTimer.value) {
    clearTimeout(trackTimer.value)
    trackTimer.value = null
  }

  // 清理轨迹动画
  if (carTrack.value) {
    carTrack.value.stop()
    carTrack.value = null
  }

  // 移除事件监听
  mapInstance.value?.removeEventListener('click', handleMapClick)
  mapInstance.value?.removeEventListener('mousemove', handleMouseMove)
}

// 组件卸载时清理
onUnmounted(() => {
  cleanupResources()
})

九、关键技术难点与解决方案

9.1 坐标系转换问题

天地图默认使用 CGCS2000 坐标系,与 GPS 的 WGS84 坐标系有偏移:

typescript
// 坐标转换工具函数
const convertCoordinates = (
  lng: number,
  lat: number,
  fromType: string,
  toType: string
) => {
  // 实际项目中建议使用专业的坐标转换库
  // 如 coordtransform 或调用天地图坐标转换API
  return { lng: convertedLng, lat: convertedLat }
}

9.2 事件冲突处理

绘制模式下需要阻止地图默认行为:

typescript
const enableDrawingMode = () => {
  if (mapInstance.value) {
    mapInstance.value.disableDrag() // 禁用拖拽
    mapInstance.value.disableDoubleClickZoom() // 禁用双击缩放
  }
}

const disableDrawingMode = () => {
  if (mapInstance.value) {
    mapInstance.value.enableDrag()
    mapInstance.value.enableDoubleClickZoom()
  }
}

9.3 移动端适配

typescript
// 触摸事件兼容
const bindMobileEvents = () => {
  if ('ontouchstart' in window) {
    mapInstance.value?.addEventListener('touchstart', handleTouchStart)
    mapInstance.value?.addEventListener('touchmove', handleTouchMove)
    mapInstance.value?.addEventListener('touchend', handleTouchEnd)
  }
}

十、使用示例与最佳实践

10.1 基础使用

vue
<template>
  <div class="map-container">
    <TianMap
      ref="mapRef"
      :controls="1"
      @map-ready="onMapReady"
      class="h-full"
    />
  </div>
</template>

<script setup>
import { TianMap } from '@/components/TianMap'

const mapRef = ref()

const onMapReady = () => {
  console.log('地图初始化完成')
  loadVehicleData()
}

const loadVehicleData = async () => {
  const vehicles = await fetchVehicleList()
  mapRef.value?.setCarList(vehicles)
}
</script>

10.2 最佳实践建议

  1. API 密钥管理:使用环境变量管理,避免硬编码
  2. 错误处理:完善的降级方案和用户友好的错误提示
  3. 性能监控:大数据量时的渲染性能监控
  4. 移动端优化:响应式设计和触摸交互优化
  5. 数据缓存:合理使用缓存减少 API 调用

十一、总结

天地图在 GIS 应用中提供了强大的基础能力,但在实际项目中需要处理诸多细节问题。通过完善的类型定义、合理的组件架构、性能优化策略以及细致的交互体验设计,可以构建出功能强大且用户体验良好的地图应用。

关键成功要素:

  • 系统的 API 封装:提供类型安全的接口
  • 模块化设计:功能独立、便于维护
  • 性能优化:大数据量处理和内存管理
  • 用户体验:直观的交互和及时的反馈
  • 兼容性考虑:多平台和多设备适配

本项目的天地图组件实现可作为类似项目的参考基础,根据具体需求进行扩展和优化。

声明

作者:J

版权:此文章版权归 J 所有,如有转载,请注明出处!

链接:可点击右上角分享此页面复制文章链接

上次更新时间:

最近更新