在现代地理信息系统(GIS)开发中,轨迹回放功能是可视化移动对象(如车辆、船只、无人机等)运动路径的关键工具。它不仅能够帮助用户直观地了解对象的运动轨迹,还能通过动态展示增强用户体验。然而,许多开发者在实现这一功能时,往往会依赖于第三方插件,而忽略了纯 JavaScript 实现的灵活性和性能优势。
本文将深入探讨如何使用 Leaflet 和原生 JavaScript 实现轨迹回放功能。我们将从地图初始化、轨迹数据准备、动画实现以及方向计算等多个方面进行详细解析。这种方法虽然代码量稍多,但提供了更高的灵活性和性能优化空间,尤其适合对性能和功能有较高要求的项目。
一、整体设计思路
在实现轨迹回放功能时,我们需要完成以下几个关键步骤:
-
地图初始化:构建一个稳定的基础地图环境,加载地图瓦片并设置初始状态。
-
轨迹数据准备:对轨迹数据进行格式化和坐标转换,确保其与 Leaflet 兼容。
-
轨迹动画实现:通过定时器逐步更新标记的位置,并根据轨迹方向动态调整标记的旋转角度。
-
动态方向计算:实现一个算法,动态计算标记的旋转角度,使其始终指向运动方向。
-
性能优化:通过合理设置动画参数和减少不必要的 DOM 操作,提升性能和用户体验。
二、具体实现步骤
地图初始化
在实现轨迹回放之前,我们需要构建一个稳定的基础地图环境。Leaflet 是一个轻量级且功能强大的地图库,支持多种地图瓦片源。本文中,我们选择使用天地图(Tianditu)作为地图数据源。以下是初始化地图的核心代码:
import L from "leaflet";
import "leaflet-rotatedmarker"; // 用于图标旋转
export default {
data() {
return {
map: null,
};
},
mounted() {
this.initMap();
},
methods: {
initMap() {
this.map = L.map("mapViewRef", {
center: [23.120555, 113.324553], // 地图中心点
zoom: 15, // 初始缩放比例
zoomControl: false, // 禁用默认缩放控件
doubleClickZoom: false, // 禁用双击放大
attributionControl: false, // 禁用默认版权信息
minZoom: 3, // 最小缩放级别
});
const tiandiKey = "YOUR_TIANDITU_KEY"; // 替换为你的天地图密钥
const mapUrl = `http://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandiKey}`;
const cvaLayer = L.tileLayer(
`http://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tiandiKey}`
);
L.tileLayer(mapUrl).addTo(this.map); // 添加矢量地图层
cvaLayer.addTo(this.map); // 添加注记层
},
},
};
说明:初始化地图时,我们禁用了默认的缩放控件和双击放大功能,以避免与自定义控件冲突。同时,通过天地图的矢量地图层和注记层,我们能够提供更丰富的地图信息。
轨迹数据准备
轨迹数据通常以经纬度数组的形式提供,但在实际应用中,这些数据可能来自不同的坐标系(如 GCJ-02、WGS-84 等)。为了确保数据的准确性,我们需要将这些坐标统一转换为 Leaflet 支持的 WGS-84 坐标系。以下是数据准备的核心代码:
import gcoord from "gcoord";
export default {
data() {
return {
trackList: [
{
latitude: "23.124336",
longitude: "113.315739",
creatorTime: "2024-07-20 02:40:04"
},
{
latitude: "23.123918",
longitude: "113.320863",
creatorTime: "2024-07-21 02:40:04"
},
{
latitude: "23.119572",
longitude: "113.321034",
creatorTime: "2024-07-22 02:40:04"
}
....
],
};
},
mounted() {
// 转换轨迹坐标数据
this.trackList.forEach((item) => {
// 转换坐标系(将 GCJ-02 坐标转换为 WGS-84 坐标)
const [longitude, latitude] = gcoord.transform(
[item.longitude, item.latitude],
gcoord.GCJ02,
gcoord.WGS84
);
// 更新转换后的坐标
item.longitude = longitude;
item.latitude = latitude;
});
},
}
说明:gcoord
是一个常用的地理坐标转换工具,支持 GCJ-02(火星坐标系)、WGS-84(全球定位系统坐标系)等多种坐标系之间的转换。通过 gcoord.transform
方法,我们将原始轨迹数据的坐标转换为 WGS-84 坐标系,以确保与 Leaflet 的兼容性。
轨迹动画实现
轨迹动画的核心是通过定时器逐步更新标记的位置,并根据轨迹方向动态调整标记的旋转角度。以下是动画实现的关键代码:
animateTrack(trackList) {
const segments = trackList.map((point, index) => ({
start: L.latLng(point.lat, point.lng),
end: L.latLng(trackList[index + 1]?.lat, trackList[index + 1]?.lng),
}));
const speed = 0.05; // 动画速度
const interval = 20; // 更新间隔
const animate = () => {
if (!this.isAnimationRunning) return;
const segment = segments[this.segmentIndex];
const latLng = L.latLng(
segment.start.lat + this.progress * (segment.end.lat - segment.start.lat),
segment.start.lng + this.progress * (segment.end.lng - segment.start.lng)
);
this.shipMarker.setLatLng(latLng);
this.shipMarker.setRotationAngle(this.calculateBearing(segment.start, segment.end));
if (this.progress >= 1) {
this.progress = 0;
this.segmentIndex++;
} else {
this.progress += speed;
}
this.animationTimeout = setTimeout(animate, interval);
};
animate();
},
说明:动画的核心逻辑是通过 setInterval
或 setTimeout
定时器逐步更新标记的位置。我们通过线性插值计算标记在当前线段上的位置,并动态调整标记的旋转角度以匹配轨迹方向。
动态方向计算
轨迹动画中,标记的方向需要根据轨迹的走向动态调整。以下是计算方向的核心算法:
calculateBearing(start, end) {
const startLat = this.degreesToRadians(start.lat);
const startLng = this.degreesToRadians(start.lng);
const endLat = this.degreesToRadians(end.lat);
const endLng = this.degreesToRadians(end.lng);
const dLng = endLng - startLng;
const y = Math.sin(dLng) * Math.cos(endLat);
const x = Math.cos(startLat) * Math.sin(endLat) - Math.sin(startLat) * Math.cos(endLat) * Math.cos(dLng);
const bearing = this.radiansToDegrees(Math.atan2(y, x));
return (bearing + 360) % 360; // 归一化到 0-360 度
},
degreesToRadians(degrees) {
return degrees * Math.PI / 180;
},
radiansToDegrees(radians) {
return radians * 180 / Math.PI;
},
说明:方向计算基于地理坐标系中的球面三角学。calculateBearing
函数通过计算两点之间的方位角,动态调整标记的旋转角度,从而实现标记始终指向运动方向的效果。
性能优化
在实现轨迹回放功能时,性能优化和用户体验是两个不可忽视的方面。以下是优化建议:
-
合理设置动画速度和更新间隔:通过调整
speed
和interval
参数,可以确保动画的流畅性。较低的速度和较短的间隔可以使动画更加平滑,但可能会增加计算负担。 -
动态调整标记的旋转角度:通过实时计算方向角,标记始终指向运动方向,提升了视觉效果。
-
减少不必要的 DOM 操作:避免频繁地添加或移除地图元素,而是通过更新现有元素的属性来实现动态效果。
三、总结
通过以上步骤,我们成功实现了一个基于 Leaflet 和原生 JavaScript 的轨迹回放功能。这种方法的优点在于高度的灵活性和定制化能力,能够满足复杂业务场景的需求。然而,它也存在一定的缺点,比如代码量较大,且需要手动处理动画的细节。
在后续的文章中,我将分享如何使用 Leaflet 的插件 leaflet-trackplayer
来实现类似的功能,这将大大简化代码并提高开发效率。敬请期待!
四、扩展阅读
-
Leaflet 官方文档:Leaflet - a JavaScript library for interactive maps
-
gcoord 坐标转换工具:https://github.com/allenhwkim/gcoord
-
Leaflet 插件推荐:Plugins - Leaflet - a JavaScript library for interactive maps