记录openlayers实现轨迹回放功能(播放,暂停,进度条控制,重播)

记录openlayers实现轨迹回放功能(播放,暂停,进度条控制,重播)

实现效果如下

主要使用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

转载请说明出处内容投诉
CSS教程网 » 记录openlayers实现轨迹回放功能(播放,暂停,进度条控制,重播)

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买