2.2.0 • Published 4 months ago

leaflet-trackline v2.2.0

Weekly downloads
-
License
ISC
Repository
-
Last release
4 months ago

leaflet-trackline


一个基于leaflet开发轨迹实时、历史插件,帮助你快速构建出精美的轨迹回放功能,之前使用的是leaflet-trackplayer,但由于他没有实时轨迹和数据点超过2万个时会出现卡顿,于是便开发这个插件,由于水平和时间有限,代码命名和写法不是很规范,后续会持续迭代更新,还请留下宝贵的意见。


  • 历史轨迹

输入图片说明

  • 实时轨迹

输入图片说明

仓库地址

leaflet-trackline:https://gitee.com/yufuhuang/leaflet-trackline

leaflet-trackline-demo: https://gitee.com/yufuhuang/leaflet-trackline-demo

使用方式

const TrackLine = new L.TrackLine(trackList,option).addTo(map);

代码示例

<template>
  <div id="tiandi-map" class="map" style="width: 100%; height: 100%"></div>
</template>

<script setup>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "@/utils/leaflet.ChineseTmsProviders.js";
import '@/utils/leaflet-trackline';

let map,TrackLine;
const emits = defineEmits();

function initMap() {
  const basemapLayer0 = L.tileLayer.chinaProvider('TianDiTu.Normal.Map');
  const basemapLayer1 = L.tileLayer.chinaProvider('TianDiTu.Normal.Annotion');
  const basemapLayer2 = L.tileLayer.chinaProvider('TianDiTu.Satellite.Map');
  const basemapLayer3 = L.tileLayer.chinaProvider('TianDiTu.Satellite.Annotion');

  const basemap1 = L.layerGroup([basemapLayer0, basemapLayer1]);
  const basemap2 = L.layerGroup([basemapLayer2,basemapLayer3]);
  map = L.map('tiandi-map', {
    preferCanvas: true,
    zoomControl: false,
    zoomAnimation: true,
    layers: [basemap2],
    doubleClickZoom: true,
    attributionControl: false,
    minZoom: 5,
    maxZoom: 18
  }).setView([31.815908, 117.185687], 8);         
}

function initTrackLine(trackList,option){
  TrackLine = new L.TrackLine(trackList,option).addTo(map);
}

function updateTrack(trackList,location){
  TrackLine.updateTrack(trackList,location)
}

function start(index){
  TrackLine.start(index); 
  TrackLine.eventEmitter.addEventListener('progress', eventListener); 
}

function pause(){
  TrackLine.pause();
}

function setProgress(index){
  TrackLine.setProgress(index); 
}

function setSpeed(index,speed){  
  TrackLine.setSpeed(index,speed)
}

function eventListener(event){
  emits('changeSliding',event.detail);
}

defineExpose({
    initTrackLine,
    start,
    pause,
    setProgress,
    setSpeed,
    updateTrack
})

onMounted(() => {
  initMap();  
});

onBeforeUnmount(() => {
  TrackLine.eventEmitter.removeEventListener('progress', eventListener);
  TrackLine.remove();
});
</script>

代码demo

<template>
  <div class="home">
    <div id="tiandi-map" class="map" style="width: 100%; height: 100%"></div>

    <div class="track-control" :class="isDesc?'track-control-20':'track-control-410'">
      <img v-show="isPlay" src="@/assets/images/work/bofang.png" alt="播放" @click="handlePlay">
      <img v-show="!isPlay" src="@/assets/images/work/zanting.png" alt="暂停" @click="handlePlay">
      <img class="ml10" src="@/assets/images/work/tingzhi.png" alt="停止" @click="handlePause">
      <div class="time-text ml10 mr20">{{ formattedTimes[sliding] || '00:00:00' }}</div>  
      <el-slider :disabled="isDisabled" @change="handleSliding" style="width:210px;" v-model="sliding" :min="0" :max="formattedTimes.length>0?formattedTimes.length-1:0" :show-tooltip="false"></el-slider>    
      <div class="time-text ml10">{{ formattedTimes[formattedTimes.length-1] || '00:00:00' }}</div>
      <el-select :disabled="isDisabled" class="ml10" size="small" style="width:160px;" v-model="speed" @change="handleSpeed" placeholder="请选择">
        <el-option
          v-for="item in speedOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value">
        </el-option>
      </el-select>
      <el-checkbox v-if="isDisabled" class="ml20" v-model="location" @change="handleLocation">实时定位</el-checkbox>   
      <div class="desc">
        <img v-show="isDesc" src="@/assets/images/work/desc.png" @click="handleDesc">
        <img v-show="!isDesc" src="@/assets/images/work/desc-active.png" @click="handleDesc">
      </div>     
    </div>

    <div class="track-detail-dom" v-if="!isDesc">
      <el-table v-loading="tableLoading" :data="tablePointList" :height="330">
        <el-table-column label="序号" align="center" type="index" width="100" />
        <el-table-column label="定位时间" align="center" prop="createTime"/>
        <el-table-column label="定位经度" align="center" prop="lng"/>
        <el-table-column label="定位纬度" align="center" prop="lat"/>
      </el-table>
    </div>
  </div>
</template>

<script setup name="Index">
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import 'leaflet.chinatmsproviders';
import 'leaflet-trackline';
import { trackArr } from './track.js';
const { proxy } = getCurrentInstance();
import startIconUrl from '@/assets/images/work/start.png';
import endIconUrl from '@/assets/images/work/end.png';
import markerIconUrl from '@/assets/images/work/jq.png';

let map,TrackLine;
const emits = defineEmits();

const isDisabled = ref(false);
const startTime = ref('00:00:00');
const endTime = ref('00:00:00');
const sliding = ref(0);
const speed = ref(500);
const point = ref(false);
const location = ref(true);
const isPlay = ref(true);
const isDesc = ref(true);
const formattedTimes = ref([]);
const tableLoading = ref(false);
const tablePointList = ref([]);
let timing = null;
const distance = 10;
const earthRadius = 6371000;

const speedOptions = ref([
  {
    label:'播放速度(16x)',
    value: 31
  },
  {
    label:'播放速度(8x)',
    value: 62
  },
  {
    label:'播放速度(4x)',
    value: 125
  },
  {
    label:'播放速度(2x)',
    value: 250
  },
  {
    label:'播放速度(正常)',
    value: 500
  }
]);

function initMap() {
  const basemapLayer0 = L.tileLayer.chinaProvider('TianDiTu.Normal.Map');
  const basemapLayer1 = L.tileLayer.chinaProvider('TianDiTu.Normal.Annotion');
  const basemapLayer2 = L.tileLayer.chinaProvider('TianDiTu.Satellite.Map');
  const basemapLayer3 = L.tileLayer.chinaProvider('TianDiTu.Satellite.Annotion');

  const basemap1 = L.layerGroup([basemapLayer0, basemapLayer1]);
  const basemap2 = L.layerGroup([basemapLayer2,basemapLayer3]);
  map = L.map('tiandi-map', {
    preferCanvas: true,
    zoomControl: false,
    zoomAnimation: true,
    layers: [basemap2],
    doubleClickZoom: true,
    attributionControl: false,
    minZoom: 5,
    maxZoom: 18
  }).setView([31.815908, 117.185687], 18);         
}

function getList(){
  tableLoading.value = true;
  formattedTimes.value = splitTimesIntoStopwatchFormat(trackArr); 
  const option = {
    startIconOptions: {iconUrl:startIconUrl,iconSize:[25, 32]},
    endIconOptions: {iconUrl:endIconUrl,iconSize:[25, 32]},
    markerIconOptions: {iconUrl:markerIconUrl,iconSize:[25, 32]},
    weight: 4,
    location: isDisabled.value,
    speed: 500,
    notPassedLineOptions: { weight:4, color: "#FB7B01", opacity: 1 },
    passedLineOptions: { weight:4, color: "#0FE217", opacity: 1 }
  }
  tablePointList.value = trackArr;
  TrackLine = new L.TrackLine(trackArr,option).addTo(map);
  if (isDisabled.value) {      
    getRealPoint();       
  }   
  tableLoading.value = false;  
}

function getRealPoint(){
  timing = setInterval(() => {
    const { lng,lat } = trackArr[trackArr.length-1];
    const newPosition = calculateNewPosition(lat, lng, distance, randomInt(0, 90));
    tablePointList.value.push(newPosition);
    formattedTimes.value = splitTimesIntoStopwatchFormat(tablePointList.value);
    sliding.value = formattedTimes.value.length - 1;
    TrackLine.updateTrack(tablePointList.value,location.value)
  }, 1000);
}

function handleLocation(flag){
  location.value = flag;
  if (isDisabled.value && location.value) {
    if (timing) {
      clearInterval(timing);
      timing = null;
    }    
    getRealPoint();
  }
}

const randomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

function calculateNewPosition(lat, lon, distance, bearing) {
  const lat1 = (lat * Math.PI) / 180;
  const lon1 = (lon * Math.PI) / 180;
  const brng = (bearing * Math.PI) / 180;
  const d = distance / earthRadius;
  const lat2 = Math.asin(Math.sin(lat1) * Math.cos(d) +
                         Math.cos(lat1) * Math.sin(d) * Math.cos(brng));
  const lon2 = lon1 + Math.atan2(Math.sin(brng) * Math.sin(d) * Math.cos(lat1),
                                 Math.cos(d) - Math.sin(lat1) * Math.sin(lat2));

  return {
    createTime:proxy.parseTime(new Date()),
    lat: (lat2 * 180) / Math.PI,
    lng: (lon2 * 180) / Math.PI
  };
}

function convertToStopwatchFormat(ms) {
  const hours = Math.floor(ms / (1000 * 60 * 60));
  const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60));
  const seconds = Math.floor((ms % (1000 * 60)) / 1000);
  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

function parseDate(dateStr) {
  return new Date(dateStr.replace(' ', 'T') + 'Z');
}

function splitTimesIntoStopwatchFormat(data) {
  const times = data.map(item => parseDate(item.createTime));      
  const startTime = new Date(Math.min(...times));
  const endTime = new Date(Math.max(...times));      
  const totalDurationMs = endTime - startTime;
  const segmentDurationMs = totalDurationMs / (times.length - 1);
  const formattedTimes = [];
  for (let i = 0; i < times.length; i++) {
    const segmentTimeMs = startTime.getTime() + (segmentDurationMs * i);
    const formattedTime = convertToStopwatchFormat(segmentTimeMs - startTime.getTime());
    formattedTimes.push(formattedTime);
  }
  return formattedTimes;
}

function handlePlay(){      
  if (isDisabled.value) return false;
  if (isPlay.value) {
    TrackLine.start(sliding.value); 
    TrackLine.eventEmitter.addEventListener('progress', eventListener); 
  }else{
    TrackLine.pause();        
  }
  isPlay.value = !isPlay.value;
}

function handlePause(){
  if (isDisabled.value) return false;
  TrackLine.pause();      
  TrackLine.setProgress(0); 
  sliding.value = 0;
  isPlay.value = true;
}

function handleSpeed(){
 TrackLine.setSpeed(sliding.value,speed.value)        
}

function changeSliding(index){  
  sliding.value = index;
  if (index == formattedTimes.value.length-1) {
    isPlay.value = true;
  }
}

function handleSliding(){
  TrackLine.setProgress(sliding.value);     
}

function handleDesc(){
  isDesc.value = !isDesc.value;
}

function eventListener(event){
  changeSliding(event.detail);
}

onMounted(() => {
  initMap();
  getList();  
});

onBeforeUnmount(() => {
  if (timing) {
    clearInterval(timing);
  }
  TrackLine.eventEmitter.removeEventListener('progress', eventListener);
  TrackLine.remove();
});
</script>

<style scoped lang="scss">
.home{
  width: 100vw;
  height: 100vh;
  position: absolute;  
  left: 0;
  top: 0;
}

.track-control{
  position: absolute;
  left: 50%;
  z-index: 999;
  width: 650px;
  height: 50px;        
  transform: translate(-50%);
  background: #fff;
  border-radius: 24px;
  padding: 0 20px;
  font-size: 14px;
  color: #424242;
  display: flex;
  align-items: center;
  justify-content: space-between;
  img{
    cursor: pointer;
  }
  .time-text{
    color: #1eac63;
  }
  .desc{
    position: absolute;
    right: -10px;
    transform: translateX(100%);
  }
  .ml10{
    margin-left: 10px;    
  }
  .mr10{
    margin-right: 10px;    
  }
  .ml20{
    margin-left: 20px;
  }
  .mr20{
    margin-right: 20px;
  }
}

.track-control-20{
  bottom: 20px;
}

.track-control-410{
  bottom: 410px;
}

.track-detail-dom{
  position: absolute;
  left: 50%;
  bottom: 20px;
  transform: translate(-50%);
  z-index: 999;
  width: calc(100% - 100px);
  height: 360px;
  background-color: #fff;
  border-radius: 10px;
  padding: 10px 20px;
}
</style>

文档说明

参数说明

选项类型默认值描述
option.startIconOptionsL.icon.options-轨迹开始图标配置
option.endIconOptionsL.icon.options-轨迹结束图标配置
option.markerIconOptionsL.icon.options-小车图标配置
option.speedNumber500轨迹线宽度
option.locationBooleanfalsetrue:实时轨迹,false:历史轨迹
option.notPassedLineOptionsL.polyline.options-未经过轨迹线配置
option.passedLineOptionsL.polyline.options-经过轨迹线配置

方法

方法返回值描述
addTo(map)-加载轨迹
updateTrack(trackList,location)-更新实时轨迹
start(index)-历史轨迹播放开始
pause()-历史轨迹播放停止
setSpeed(index,speed)-设置历史轨迹播放速度
setProgress(index)-设置历史轨迹进度
remove-清除所有

事件

事件描述
progress进度改变事件

更新日志

V2.2.0(2025-02-12)

  • 新增示例demo
  • 优化代码
  • 修复已知问题

V2.1.0(2025-01-16)

  • 新增配置项
  • 优化代码
  • 新增清除方法
  • 修复已知问题

V2.0.0(2025-01-14)

  • 使用类方式重构

V1.0.3(2024-11-28)

  • 修改依赖包未引入问题

V1.0.2(2024-11-28)

  • 修改小车方向方法,解决小车方向不正确问题
  • 修复已知问题

V1.0.1(2024-11-27)

  • 优化文档和一些细节处理

V1.0.0(2024-11-27)

  • leaflet-trackline正式发布

🎉致谢与引用

我对以下开源插件深表感谢,它们为本插件的功能提供了关键支持。

  • leafletjs —— Leaflet 是一个开源并且对移动端友好的交互式地图 JavaScript 库
2.2.0

4 months ago

2.1.0

4 months ago

2.0.0

4 months ago

1.0.3

6 months ago

1.0.2

6 months ago

1.0.1

6 months ago

1.0.0

6 months ago