实现效果如下
主要使用openlayers 10.6.0的版本开发,底图使用的是天地图影像图和注记两个图层,整体功能不算复杂主要分析一下几点:
- 获取路径经纬度数组,处理经纬度信息
- 绘制路径,小车,起点,终点
- 让车子动起来
- 实现开始,暂停,重播,进度条控制,速度控制
一、初始化地图
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import { Vector as VectorSource, XYZ } from 'ol/source';
import { Tile, Vector as VectorLayer } from 'ol/layer';
class olMap {
constructor(option = {}) {
this.option = option
this.initMap()
}
// 初始化地图
initMap = () => {
const { mapId, projection, layers = [], center, maxZoom, zoom } = this.option
this.mapOL = new Map({
target: mapId || 'map',
layers: [
new Tile({
source: new XYZ({
url: 'http://t0.tianditu.gov.***/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + {你自己的tk},
crossOrigin: "anoymous"
})
}),
new Tile({
source: new XYZ({
url: 'http://t0.tianditu.gov.***/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + {你自己的tk},
crossOrigin: "anoymous"
})
}),
...layers
],
view: new View({
center: fromLonLat(center || [116.404, 39.915]), // 默认北京经纬度
maxZoom: maxZoom || 18, // 最大缩放级别
zoom: zoom || 7,
projection: projection || 'EPSG:3857'
// projection: 'EPSG:3857' // 默认使用Web Mercator投影
})
});
window.mapOL = this.mapOL;
}
}
二、获取路径经纬度数组,处理经纬度信息
因为地图默认使用的是ESGP:3857墨卡托投影,所以要把给的经纬度坐标转化一下,比较简单
import { fromLonLat } from 'ol/proj';
// 路径坐标转换
const pathArr = pathData.map(item => fromLonLat(item))
// 当然你也可以用其它的方法去转换, 比如 transform
三、绘制路径,小车,起点,终点
// 全部轨迹路径
trackLine = new LineString(pathArr);
const trackLineFeature = new Feature(trackLine)
trackLineFeature.setStyle(new Style({
stroke: new Stroke({
color: trackStyle.color || 'rgba(0, 151, 255, 1)',
width: 5,
lineDash: [5, 10],
lineCap: 'round'
}),
fill: new Fill({
color: trackStyle.bgColor || 'red'
})
}));
// 经过后的轨迹路径
passTrackLineFeature = new Feature(new LineString([]))
passTrackLineFeature.setStyle([new Style({
stroke: new Stroke({
color: passTrackStyle.color || 'rgba(38, 158, 238, 1)',
width: 9
}),
}), new Style({
stroke: new Stroke({
color: passTrackStyle.color || 'rgb(19, 95, 145)',
width: 5
}),
})]);
// 添加起点和终点标记
const startFeature = new Feature({
geometry: new Point(trackLine.getFirstCoordinate()),
name: 'Start'
});
startFeature.setStyle(new Style({
image: new Icon({
src: startStyle.icon,
width: startStyle.width || 32, // 图标宽度
height: startStyle.height || 32, // 图标高度
anchor: startStyle.offset || [0.8, 0.8] // 图标锚点
})
}));
const endFeature = new Feature({
geometry: new Point(trackLine.getLastCoordinate()),
name: 'End'
});
endFeature.setStyle(new Style({
image: new Icon({
src: endStyle.icon,
width: endStyle.width || 32, // 图标宽度
height: endStyle.height || 32, // 图标高度
anchor: endStyle.offset || [0.2, 1.1] // 图标锚点
})
}));
// 小车
carFeature = new Feature({
id: 'Car', // 设置ID以便后续动画更新
geometry: new Point(trackLine.getFirstCoordinate()),
name: 'Car'
});
carFeature.setStyle(new Style({
image: new Icon({
src: carStyle.icon,
rotation: carStyle.rotation || (-getCarRotation(pathArr[0], pathArr[1])),
scale: carStyle.scale || 0.3 // 调整图标大小
})
}));
四、更新汽车位置和路径
const updateCarPosition = () => {
const currentCoordinate = allCoordinates[index];
// 计算车头旋转角度
let lastPoint = [], currentPoint = []
if (index + 1 > allCoordinates.length - 1) {
lastPoint = allCoordinates[index]
currentPoint = allCoordinates[index - 1]
} else {
lastPoint = allCoordinates[index + 1]
currentPoint = allCoordinates[index];
}
// let dx = currentPoint[0] - lastPoint[0];
// let dy = currentPoint[1] - lastPoint[1];
let rotation = getCarRotation(currentPoint, lastPoint); // 直接使用弧度值,无需转换为角度
carFeature.getStyle().getImage().setRotation(-rotation)
// 设置小车的位置
carFeature.getGeometry().setCoordinates(currentCoordinate);
// 暂停时设置经过的路径(进度条拖动)
if (isPlay) {
const arr = allCoordinates.slice(0, index)
passTrackLineFeature.getGeometry().setCoordinates(arr)
return
}
// 播放时设置经过的路径
if (index == 0) {
passTrackLineFeature.getGeometry().setCoordinates([])
} else {
passTrackLineFeature.getGeometry().appendCoordinate(currentCoordinate)
}
}
五、计算车头角度
主要使用Math.atan2 方法计算两个相邻坐标点的夹角,在设置icon的rotation属性时正北方向为0度
可以通过加减Math.PI 调整旋转方向
const getCarRotation = (firstPoint, lastPoin) => {
let dx = firstPoint[0] - lastPoin[0];
let dy = firstPoint[1] - lastPoin[1];
let rotation = Math.PI / 2 + Math.atan2(dy, dx); // 直接使用弧度值,无需转换为角度
return rotation
}
六、让车子动起来
这里主要使用setInterval去实现的动画,你还可以使用postrender , requestAnimationFrame等去实现动画
// 轨迹动画
const animate = () => {
if (timeId) {
clearInterval(timeId)
}
timeId = setInterval(() => {
if (index > allCoordinates.length - 1) {
stop()
timeId = null
index = 0; // 重置索引
return;
}
// 更新移动目标位置
updateCarPosition()
// 更新进度
progeress = Math.floor((index / (allCoordinates.length - 1)) * 100)
update && update(progeress)
index++;
}, speed)
}
七、实现开始,暂停,重播,进度条控制,速度控制
// 暂停轨迹动画
const stop = () => {
isPlay = true
if (timeId) {
clearInterval(timeId)
}
}
// 播放轨迹动画
const play = () => {
animate()
}
// 重播
const reset = () => {
if (timeId) {
clearInterval(timeId)
timeId = null
index = 0
}
animate()
}
// 拖动播放轴更新小车位置
const upDateCar = (val) => {
stop()
const ids = Math.floor((val / 100) * (allCoordinates.length - 1))
index = ids
updateCarPosition()
}
// 改变播放速度
const upDateStep = (val) => {
speed = val
stop()
play()
}
完整代码
class olMap {
constructor(option = {}) {
this.option = option
this.initMap()
}
// 初始化地图
initMap = () => {
const { mapId, projection, layers = [], center, maxZoom, zoom } = this.option
this.mapOL = new Map({
target: mapId || 'map',
layers: [
new Tile({
source: new XYZ({
url: 'http://t0.tianditu.gov.***/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tk,
crossOrigin: "anoymous"
})
}),
new Tile({
source: new XYZ({
url: 'http://t0.tianditu.gov.***/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + tk,
crossOrigin: "anoymous"
})
}),
...layers
],
view: new View({
center: fromLonLat(center || [116.404, 39.915]), // 默认北京经纬度
maxZoom: maxZoom || 18, // 最大缩放级别
zoom: zoom || 7,
projection: projection || 'EPSG:3857'
// projection: 'EPSG:3857' // 默认使用Web Mercator投影
})
});
this.mapOL.on('click', (evt) => {
this.mapOL.forEachFeatureAtPixel(evt.pixel, (feature) => {
// console.log('选中:', feature.get('data'));
if (feature.get('isClick')) {
this.mapClick && this.mapClick(feature.get('data'))
}
return true; // 停止继续检查其他feature
});
})
// const select = new Select()
// this.mapOL.addInteraction(select)
window.mapOL = this.mapOL;
}
/**
* 路径回放功能
* @param pathData 路径数据(经纬度)
*/
animationPath = (option = {}) => {
const { pathData, passTrackStyle = {}, trackStyle = {}, startStyle = {}, endStyle = {}, carStyle = {}, update, step = 10 } = option
if (!pathData.length) {
return
}
// 路径坐标转换
const pathArr = pathData.map(item => fromLonLat(item))
let trackLine = null; // 路径线对象
let carFeature = null; // 汽车图标对象
let passTrackLineFeature = null // 经过的路径
let trackLayer = null; // 路径图层
let index = 0; // 当前路径点索引
let progeress = 0; // 进度
let allCoordinates = []; // 插值后的所有坐标点数组
let timeId = null; // 定时器ID
let isPlay = false; //是否暂停了
let speed = 20 //速度
// 根据长度计算插值点
const addTrack = () => {
// 轨迹在投影平面上的长度
const trackLineLen = trackLine.getLength();
// 当前平面的分辨率
const resolution = mapOL.getView().getResolution();
// 点有可能是小数,要到终点需要手动添加最后一个点
const pointCount = trackLineLen / (resolution * 1); // 每10米一个点
for (let i = 0; i <= pointCount; i++) {
allCoordinates.push(trackLine.getCoordinateAt(i / pointCount));
}
allCoordinates.push(pathArr.at(-1));
};
// 计算车头角度
const getCarRotation = (firstPoint, lastPoin) => {
let dx = firstPoint[0] - lastPoin[0];
let dy = firstPoint[1] - lastPoin[1];
let rotation = Math.PI / 2 + Math.atan2(dy, dx); // 直接使用弧度值,无需转换为角度
return rotation
}
// 绘制路径
const drwglinePath = () => {
// 全部轨迹路径
trackLine = new LineString(pathArr);
const trackLineFeature = new Feature(trackLine)
trackLineFeature.setStyle(new Style({
stroke: new Stroke({
color: trackStyle.color || 'rgba(0, 151, 255, 1)',
width: 5,
lineDash: [5, 10],
lineCap: 'round'
}),
fill: new Fill({
color: trackStyle.bgColor || 'red'
})
}));
// 经过后的轨迹路径
passTrackLineFeature = new Feature(new LineString([]))
passTrackLineFeature.setStyle([new Style({
stroke: new Stroke({
color: passTrackStyle.color || 'rgba(38, 158, 238, 1)',
width: 9
}),
}), new Style({
stroke: new Stroke({
color: passTrackStyle.color || 'rgb(19, 95, 145)',
width: 5
}),
})]);
// 添加起点和终点标记
const startFeature = new Feature({
geometry: new Point(trackLine.getFirstCoordinate()),
name: 'Start'
});
startFeature.setStyle(new Style({
image: new Icon({
src: startStyle.icon,
width: startStyle.width || 32, // 图标宽度
height: startStyle.height || 32, // 图标高度
anchor: startStyle.offset || [0.8, 0.8] // 图标锚点
})
}));
const endFeature = new Feature({
geometry: new Point(trackLine.getLastCoordinate()),
name: 'End'
});
endFeature.setStyle(new Style({
image: new Icon({
src: endStyle.icon,
width: endStyle.width || 32, // 图标宽度
height: endStyle.height || 32, // 图标高度
anchor: endStyle.offset || [0.2, 1.1] // 图标锚点
})
}));
carFeature = new Feature({
id: 'Car', // 设置ID以便后续动画更新
geometry: new Point(trackLine.getFirstCoordinate()),
name: 'Car'
});
carFeature.setStyle(new Style({
image: new Icon({
src: carStyle.icon,
rotation: carStyle.rotation || (-getCarRotation(pathArr[0], pathArr[1])),
scale: carStyle.scale || 0.3 // 调整图标大小
})
}));
trackLayer = new VectorLayer({
id: 'path',
source: new VectorSource({
features: [trackLineFeature, passTrackLineFeature, startFeature, endFeature, carFeature]
}),
updateWhileAnimating: true, // 不加动画会卡顿
updateWhileInteracting: true,
});
// 清空上次的图层
if (trackLayer) {
mapOL.removeLayer(trackLayer)
}
mapOL.addLayer(trackLayer);
// 定位到路径的中心
mapOL.getView().fit(trackLine.getExtent(), {
size: mapOL.getSize(),
maxZoom: 15, // 设置最大缩放级别
padding: [200, 200, 200, 200] // 设置边距
});
addTrack()
}
drwglinePath()
// 更新汽车位置和路径
const updateCarPosition = () => {
const currentCoordinate = allCoordinates[index];
// 计算车头旋转角度
let lastPoint = [], currentPoint = []
if (index + 1 > allCoordinates.length - 1) {
lastPoint = allCoordinates[index]
currentPoint = allCoordinates[index - 1]
} else {
lastPoint = allCoordinates[index + 1]
currentPoint = allCoordinates[index];
}
// let dx = currentPoint[0] - lastPoint[0];
// let dy = currentPoint[1] - lastPoint[1];
let rotation = getCarRotation(currentPoint, lastPoint); // 直接使用弧度值,无需转换为角度
carFeature.getStyle().getImage().setRotation(-rotation)
// 设置小车的位置
carFeature.getGeometry().setCoordinates(currentCoordinate);
// 暂停时设置经过的路径(进度条拖动)
if (isPlay) {
const arr = allCoordinates.slice(0, index)
passTrackLineFeature.getGeometry().setCoordinates(arr)
return
}
// 播放时设置经过的路径
if (index == 0) {
passTrackLineFeature.getGeometry().setCoordinates([])
} else {
passTrackLineFeature.getGeometry().appendCoordinate(currentCoordinate)
}
}
// 暂停轨迹动画
const stop = () => {
isPlay = true
if (timeId) {
clearInterval(timeId)
}
}
// 播放轨迹动画
const play = () => {
animate()
}
// 重播
const reset = () => {
if (timeId) {
clearInterval(timeId)
timeId = null
index = 0
}
animate()
}
// 拖动播放轴更新小车位置
const upDateCar = (val) => {
stop()
const ids = Math.floor((val / 100) * (allCoordinates.length - 1))
index = ids
updateCarPosition()
}
// 改变播放速度
const upDateStep = (val) => {
speed = val
stop()
play()
}
// 轨迹动画
const animate = () => {
if (timeId) {
clearInterval(timeId)
}
timeId = setInterval(() => {
if (index > allCoordinates.length - 1) {
stop()
timeId = null
index = 0; // 重置索引
return;
}
// 更新移动目标位置
updateCarPosition()
// 更新进度
progeress = Math.floor((index / (allCoordinates.length - 1)) * 100)
update && update(progeress)
index++;
}, speed)
}
// 重置清空图层
const destroy = () => {
// 清空图层
if (trackLayer) {
this.mapOL.removeLayer(trackLayer)
this.mapOL.getView().setCenter(fromLonLat(this.option.center || [116.404, 39.915]))
stop()
}
}
return { stop, play, reset, upDateCar, upDateStep, timeId, isPlay, destroy }
}
}
export default olMap