Leaflet轨迹动画实现

在GIS和位置服务应用中,轨迹回放是一个常见且重要的功能。本文将详细介绍如何使用Leaflet.js和Vue.js构建一个完整的车辆历史轨迹回放系统。我们将基于一个实际的项目代码,解析从地图初始化、轨迹数据获取到动画播放的完整实现过程。
点这里下载L.MoveMarker

1. 地图初始化与配置

// 地图初始化 - 只执行一次,避免重复创建
initializeMap() {
  if (this.isMapInitialized && this.map) {
    return
  }
  
  let latlngs = [this.data[0].lat * 1, this.data[0].lng * 1]
  this.map = L.map(this.$refs['map'], {
    renderer: L.canvas(), // 使用Canvas渲染器提升性能
    preferCanvas: true,
    center: latlngs,
    zoom: this.zoom || 13,
    zoomControl: true,
    doubleClickZoom: true,
    attributionControl: false,
    maxZoom: 20,
    maxNativeZoom: 20
  })
  
  // 添加默认底图
  this.addTileLayer('moren')
  this.isMapInitialized = true
}

关键点

  • 使用Canvas渲染器提升大量轨迹点位的渲染性能
  • 通过isMapInitialized标志防止重复初始化
  • 支持多种底图类型(默认、影像、专题)

2. 轨迹数据处理流程

// 轨迹数据处理方法
processTrackData(rawData) {
  // 1. 过滤重复点位
  const arr = rawData.filter((item, index) => {
    if (index == 0) return true
    return (
      item.direction != rawData[index - 1].direction ||
      (item.lat != rawData[index - 1].lat && item.lng != rawData[index - 1].lng)
    )
  })
  
  // 2. 转换数据格式
  this.data = arr.map(item => {
    return {
      lat: item.lng * 1,
      lng: item.lat * 1,
      speed: Math.floor(item.meters * 20),
      heading: 0,
      time: dayjs(item.timeStampStr).format('YYYY-MM-DD HH:mm:ss')
    }
  })
}

3. 轨迹动画实现

3.1. 移动标记创建

createMoveMarker() {
  this.dataMarker = L.moveMarker(
    [
      [this.data[0].lng, this.data[0].lat],
      [this.data[1].lng, this.data[1].lat]
    ],
    {
      animatePolyline: true,
      color: '#0057ff',
      weight: 6,
      hidePolylines: false,
      duration: this.data[1].speed / this.currentSpeed,
      removeFirstLines: false,
      maxLengthLines: this.data[1].speed / this.currentSpeed
    },
    {
      animate: true,
      hideMarker: false,
      duration: this.data[1].speed / this.currentSpeed,
      followMarker: false,
      rotateMarker: true,
      rotateAngle: this.data[0].heading,
      icon: L.divIcon({
        className: 'position-relative rotate--marker',
        html: '<div style="width:2px; height:2px">' +
              '<img style="width: 40px;margin-top:-21px;margin-left:-21px" ' +
              'src="' + this.carImgs[this.cartype] + '" /></div>'
      })
    },
    {}
  ).addTo(this.map)
}

3.2. 动画播放控制

startTrackAnimation() {
  let num = 2
  
  const setLine = async () => {
    clearInterval(this.timer1)
    if (!this.isplay) return
    
    this.currentIndex = num - 1
    if (num < this.data.length) {
      this.timer1 = setInterval(() => {
        num++
        this.lineTime = this.data[num].minTime
        setLine()
      }, this.data[num].speed / this.currentSpeed)
      
      // 添加新的轨迹段
      this.dataMarker.addMoreLine(
        [this.data[num].lng, this.data[num].lat],
        {
          rotateAngle: this.data[num - 1].heading,
          animatePolyline: false,
          duration: this.data[num].speed / this.currentSpeed
        }
      )
    } else {
      this.clearAllTimers()
      this.isplay = false
      this.lineTime = this.maxTime
    }
  }
  
  setTimeout(() => {
    setLine()
  }, this.data[1].speed / this.currentSpeed)
}

4. 时间轴控制实现

4.1. 时间轴滑块(带防抖)

// 创建防抖函数
created() {
  this.sliderDebounce = debounce(this.handleSliderChange, 300)
}

// 滑块变化处理
onSliderChange(value) {
  this.lineTime = value
  this.sliderDebounce(value)
}

// 实际滑块变化逻辑
handleSliderChange(time) {
  if (this.isChangingTime) return
  
  this.isChangingTime = true
  this.isFirter = false
  
  // 查找当前时间对应的轨迹点
  let i = 0
  this.data.forEach((item, index) => {
    if (item.minTime <= time && time < item.maxTime) {
      i = index
    }
  })
  
  this.lineTime = this.data[i].minTime
  this.isplay = false
  this.clearAllTimers()
  this.currentIndex = i
  
  // 重新定位标记
  if (this.dataMarker) {
    this.dataMarker.stop()
    this.dataMarker.onRemove(this.dataMarker)
  }
  
  this.changeSpeed()
  this.changeMoveMarker()
  
  setTimeout(() => {
    this.isFirter = true
    this.isChangingTime = false
  }, 100)
}

5. 方向计算

calculateAngle(points) {
  var lastPrePoi = points[0]
  var lastPoi = points[1]
  
  if (lastPoi.lng == lastPrePoi.lng) {
    if (lastPoi.lat == lastPrePoi.lat) {
      return 0
    } else {
      return lastPoi.lat > lastPrePoi.lat ? 0 : 180
    }
  } else {
    // 三角函数计算角度
    let first_side_length = lastPoi.lng - lastPrePoi.lng
    let second_side_length = lastPoi.lat - lastPrePoi.lat
    let third_side_length = Math.sqrt(
      Math.pow(first_side_length, 2) + Math.pow(second_side_length, 2)
    )
    let cosine_value = first_side_length / third_side_length
    let radian_value = Math.acos(cosine_value)
    let angle_value = (radian_value * 180) / Math.PI
    
    if (angle_value < 90) {
      return second_side_length > 0 ? -0 - angle_value : -0 + angle_value
    } else {
      return second_side_length > 0 ? -0 - angle_value : angle_value - 0
    }
  }
}
posted @ 2025-12-17 17:55  战立标  阅读(1)  评论(0)    收藏  举报