前言
本文参考网上的例子,封装一个 mapbox 的轨迹回放图层,具体做的工作为:
- 将轨迹回放图层封装为一个类,可以传入自己的图标图片
- 将常用的方法暴露出来,并做了一些改进
效果
封装后的代码
import * as turf from '@turf/turf'
const svgXML =
`<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<path d="M529.6128 512L239.9232 222.4128 384.7168 77.5168 819.2 512 384.7168 946.4832 239.9232 801.5872z" p-id="9085" fill="#ffffff"></path>
</svg>
`
//给图片对象写入base64编码的svg流
const svgBase64 = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgXML)));
export default class RouteReplay {
/**
*
* @param {*} map mapbox实例对象
* @param {*} routejson 路径geojson type = lineString
* @param {*} iconImg 图标img
*/
constructor(map, routejson, iconImg) {
this.map = map
this._json = routejson
this._img = iconImg
this._animated = false
this._counter = 0
this._steps = 0
this._newRouteGeoJson = null
this._timer = null
this._layerList = ['routeLayer', 'realRouteLayer', 'arrowLayer', 'animatePointLayer']
// 车辆行进中的路线
this._realRouteGeoJson = {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': []
}
}]
}
// 小车位置点的
this._animatePointGeoJson = {
'type': 'FeatureCollection',
'features': [{
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Point',
'coordinates': []
}
}]
}
this._init()
}
_init() {
let arrowIcon = new Image(20, 20)
arrowIcon.src = svgBase64
arrowIcon.onload = () => {
// console.log(this.map)
this.map.addImage('arrowIcon', arrowIcon)
this.map.loadImage(this._img, (error, carIcon) => {
if (error) throw error;
this.map.addImage('carIcon', carIcon);
this._animatePointGeoJson.features[0].geometry.coordinates = this._json.features[0].geometry.coordinates[0]
// 小车轨迹点json
this._newRouteGeoJson = this._resetRoute(this._json.features[0], 1000, 'kilometers')
// 小车轨迹点json的点数量
this._steps = this._newRouteGeoJson.geometry.coordinates.length
this._addRoutelayer() // 添加轨迹线图层
this._addRealRouteSource() // 添加实时轨迹线图层
this._addArrowlayer() // 添加箭头图层
this._addAnimatePointSource() // 添加动态点图层
})
}
}
_animate() {
if (this._counter >= this._steps) {
return
}
let startPnt, endPnt
if (this._counter == 0) { // 开始
this._realRouteGeoJson.features[0].geometry.coordinates = []
startPnt = this._newRouteGeoJson.geometry.coordinates[this._counter]
endPnt = this._newRouteGeoJson.geometry.coordinates[this._counter + 1]
} else if (this._counter !== 0) {
startPnt = this._newRouteGeoJson.geometry.coordinates[this._counter - 1]
endPnt = this._newRouteGeoJson.geometry.coordinates[this._counter]
}
// 计算角度,用于小车的指向角度
this._animatePointGeoJson.features[0].properties.bearing = turf.bearing(
turf.point(startPnt),
turf.point(endPnt)
) - 90;
this._animatePointGeoJson.features[0].geometry.coordinates = this._newRouteGeoJson.geometry.coordinates[this._counter];
this._realRouteGeoJson.features[0].geometry.coordinates.push(this._animatePointGeoJson.features[0].geometry.coordinates)
// 小车的位置更新
this.map.getSource('animatePointLayer').setData(this._animatePointGeoJson);
// 已经走过的轨迹更新
this.map.getSource('realRouteLayer').setData(this._realRouteGeoJson);
if (this._animated) {
this._timer = requestAnimationFrame(() => { this._animate() });
}
this._counter++;
}
_addRoutelayer() {
console.log(222)
this.map.addLayer({
'id': 'routeLayer',
'type': 'line',
'source': {
'type': 'geojson',
'lineMetrics': true,
'data': this._json
},
'paint': {
'line-width': 10,
'line-opacity': 1,
'line-color': '#7ec1ff',
}
});
}
_addRealRouteSource() {
this.map.addLayer({
'id': 'realRouteLayer',
'type': 'line',
'source': {
'type': 'geojson',
'lineMetrics': true,
'data': this._realRouteGeoJson
},
'paint': {
'line-width': 10,
'line-opacity': 1,
'line-color': 'rgba(243,229,11,1)',
}
});
}
_addArrowlayer() {
this.map.addLayer({
'id': 'arrowLayer',
'type': 'symbol',
'source': {
'type': 'geojson',
'data': this._json //轨迹geojson格式数据
},
'layout': {
'symbol-placement': 'line',
'symbol-spacing': 50, // 图标间隔,默认为250
'icon-image': 'arrowIcon', //箭头图标
'icon-size': 0.5
}
});
}
// 添加动态点图层--小车
_addAnimatePointSource() {
this.map.addLayer({
'id': 'animatePointLayer',
'type': 'symbol',
'source': {
'type': 'geojson',
'data': this._animatePointGeoJson
},
'layout': {
'icon-image': 'carIcon',
'icon-size': 0.5,
'icon-rotate': ['get', 'bearing'],
'icon-rotation-alignment': 'map',
'icon-allow-overlap': true,
'icon-ignore-placement': true
}
});
}
_resetRoute(route, nstep, units) {
const newroute = {
'type': 'Feature',
'geometry': {
'type': 'LineString',
'coordinates': []
}
}
// 指定点集合的总路长
const lineDistance = turf.lineDistance(route);
// 每一段的平均距离
const nDistance = lineDistance / nstep;
const length = this._json.features[0].geometry.coordinates.length;
for (let i = 0; i < length - 1; i++) {
let from = turf.point(route.geometry.coordinates[i]); // type 为 point的feature
let to = turf.point(route.geometry.coordinates[i + 1]);
let lDistance = turf.distance(from, to, { // 两个点之间的距离
units: units
});
if (i == 0) { // 起始点直接推入
newroute.geometry.coordinates.push(route.geometry.coordinates[0])
}
if (lDistance > nDistance) { // 两点距离大于每段值,将这条线继续分隔
let rings = this._splitLine(from, to, lDistance, nDistance, units)
newroute.geometry.coordinates = newroute.geometry.coordinates.concat(rings)
} else { // 两点距离小于每次移动的距离,直接推入
newroute.geometry.coordinates.push(route.geometry.coordinates[i + 1])
}
}
return newroute
}
// 过长的两点轨迹点分段
_splitLine(from, to, distance, splitLength, units) {
var step = parseInt(distance / splitLength)
const leftLength = distance - step * splitLength
const rings = []
const route = turf.lineString([from.geometry.coordinates, to.geometry.coordinates])
for (let i = 1; i <= step; i++) {
let nlength = i * splitLength
// turf.alone返回沿着route<LineString>距离为nlength<number>的点
let pnt = turf.along(route, nlength, {
units: units
});
rings.push(pnt.geometry.coordinates)
}
if (leftLength > 0) {
rings.push(to.geometry.coordinates)
}
return rings
}
start() {
if (!this._animated) {
this._animated = true
this._animate()
}
}
pause() {
this._animated = false
this._animate()
}
end() {
this._animated = false
this._counter = 0
this._animate()
}
remove() {
window.cancelAnimationFrame(this._timer)
this._layerList.map(layer => {
// console.log(layer)
if (this.map.getSource(layer)) {
this.map.removeLayer(layer)
this.map.removeSource(layer)
}
});
}
}
在mapbox中使用
import React, { useRef, useEffect, useState } from 'react';
import mapboxgl from 'mapbox-gl';
import * as turf from '@turf/turf'
import MapboxLanguage from '@mapbox/mapbox-gl-language';
import carImg from '../../img/car2.jpg'
import RouteReplay from '../libs/routeReplay'
import routeGeoJson from '../../testData/json/routeGeoJson.json'; // 基础图层
import 'antd/dist/antd.css';
function App() {
const mapContainerRef = useRef();
const mapRef = useRef();
const routeReplayRef = useRef();
// 初始化基础图层
useEffect(() => {
mapboxgl.accessToken = 'token'
mapRef.current = new mapboxgl.Map({
center: [116.761, 39.452], // starting position [lng, lat]
zoom: 10,// starting zoom
pitch: 60,
style: 'mapbox://styles/mapbox/streets-v11',
container: mapContainerRef.current,
antialias: true,
},
);
mapRef.current.addControl(new MapboxLanguage({ defaultLanguage: "zh-Hans" }))
mapRef.current.on('load', (e) => {
routeReplayRef.current = new RouteReplay(mapRef.current, routeGeoJson, carImg)
});
}, []);
function startClick() {
routeReplayRef.current.start()
console.log(routeGeoJson)
}
function pauseClick() {
routeReplayRef.current.pause()
}
function endClick() {
routeReplayRef.current.end()
}
function removeClick() {
routeReplayRef.current.remove()
}
return (
<div style={{ display: 'flex' }}>
<div
id="map-container"
ref={mapContainerRef}
style={{ height: '100vh', width: '100vw' }}
/>
<div style={{ position: 'fixed', top: '0', right: '0' }}>
<button onClick={() => { startClick() }} style={{ marginRight: '10px' }}>开始</button>
<button onClick={() => { pauseClick() }} style={{ marginRight: '10px' }}>暂停</button>
<button onClick={() => { endClick() }} style={{ marginRight: '10px' }}>停止</button>
<button onClick={() => { removeClick() }} style={{ marginRight: '10px' }}>移除</button>
</div>
</div>
);
}
export default App;
总结
该功核心的思路在参考中都已经实现,笔者只不过在它的基础上进行了封装,代码结构还存在着优化点。
参考:mapboxgl实现带箭头轨迹线