2.2.0 • Published 4 months ago
leaflet-trackline v2.2.0
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.startIconOptions | L.icon.options | - | 轨迹开始图标配置 |
option.endIconOptions | L.icon.options | - | 轨迹结束图标配置 |
option.markerIconOptions | L.icon.options | - | 小车图标配置 |
option.speed | Number | 500 | 轨迹线宽度 |
option.location | Boolean | false | true:实时轨迹,false:历史轨迹 |
option.notPassedLineOptions | L.polyline.options | - | 未经过轨迹线配置 |
option.passedLineOptions | L.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 库