天地图使用经验总结
一、项目背景与概述
天地图作为国家地理信息公共服务平台,在物联网监控、车辆跟踪、电子围栏等场景中发挥着重要作用。本项目基于 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 轨迹播放控制难点
核心难点:
- 天地图 CarTrack 不支持暂停/恢复:只能通过重新播放实现
- 进度追踪:需要自行实现定时器追踪播放进度
- 播放完成检测:通过轮询方式检测动画是否结束
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 最佳实践建议
- API 密钥管理:使用环境变量管理,避免硬编码
- 错误处理:完善的降级方案和用户友好的错误提示
- 性能监控:大数据量时的渲染性能监控
- 移动端优化:响应式设计和触摸交互优化
- 数据缓存:合理使用缓存减少 API 调用
十一、总结
天地图在 GIS 应用中提供了强大的基础能力,但在实际项目中需要处理诸多细节问题。通过完善的类型定义、合理的组件架构、性能优化策略以及细致的交互体验设计,可以构建出功能强大且用户体验良好的地图应用。
关键成功要素:
- 系统的 API 封装:提供类型安全的接口
- 模块化设计:功能独立、便于维护
- 性能优化:大数据量处理和内存管理
- 用户体验:直观的交互和及时的反馈
- 兼容性考虑:多平台和多设备适配
本项目的天地图组件实现可作为类似项目的参考基础,根据具体需求进行扩展和优化。