一、想要的效果
1、初始效果
2、运动时效果
二、准备线路坐标点
可以使用地图拾取器系统
coordinates: [
[113.53641592772293, 34.82798925347799],
[113.55119787497061, 34.82789388575531],
[113.55920874272088, 34.825128232275],
[113.56130682738113, 34.821408898948555],
[113.5621651342659, 34.818071041751516],
[113.56197440405923, 34.808915766567786],
[113.59115683814126, 34.80662694646218],
[113.60660636206704, 34.81158605756411]
]
绘制轨迹
this.line = new LineString(this.options.coordinates) // 直线
// 线路所有坐标点坐标
let coordinates = this.options.coordinates
this.lineFeature = new ol.Feature({ // 直线线路
type: 'line',
geometry: this.line
})
this.startMarker = new ol.Feature({ // 开始点位
type: 'icon',
geometry: new ol.Point(this.line.getFirstCoordinate())
})
this.endMarker = new ol.Feature({ // 结束点位
type: 'icon',
geometry: new ol.Point(this.line.getLastCoordinate())
})
/**
* 经停点
*/
for (let i = 0; i < coordinates.length; i++) {
let s = new ol.Feature({
type: 'stop',
geometry: new ol.Point(coordinates[i])
})
this.stopMakers.push(s)
}
this.movePoint = this.startMarker.getGeometry().clone() // 移动点
this.movePointMarker = new ol.Feature({ // 移动点feature
type: 'geoMarker',
geometry: this.movePoint
})
let routeFeature = [...[this.lineFeature, this.movePointMarker, this.startMarker, this.endMarker], ...this.stopMakers]
this.trajectoryLayer = new ol.VectorLayer({ // 添加图层
source: new ol.VectorSource({
features: routeFeature,
className: 'TrajectoryLayer',
properties: this.options.layerProperties
}),
style: (feature) => {
return this.routeStyle[feature.get('type')]
}
})
this.map.addLayer(this.trajectoryLayer)
三、设置线条、经停点、导航样式
let coordinates = this.options.coordinates
let sourcePoint = coordinates[this.currentIndex]
let targetPoint = coordinates[this.currentIndex + 1]
this.routeStyle = { // 定义线路样式
line: new ol.Style({
stroke: new ol.Stroke({
width: this.options.lineStyle.width,
color: this.options.lineStyle.color
})
}),
geoMarker: new ol.Style({
image: new ol.Icon({
src: this.options.navStyle.url,
scale: this.options.navStyle.scale,
rotateWithView: false,
rotation: this.getRotation(sourcePoint, targetPoint)
})
}),
stop: new ol.Style({
image: new ol.CircleStyle({
radius: this.options.stopStyle.radius - 2,
fill: new ol.Fill({
// width: this.options.routeStyle.width,
color: this.options.stopStyle.color
}),
stroke: new ol.Stroke({
width: (this.options.stopStyle.radius - 2) / 2,
color: this.options.stopStyle.color
})
})
})
}
四、引入 动画开关、进度条、速度 控制器
this.playbarControl = new PlaybarControl(this.map, _config, (e, pos) => { // 添加播放停止动画控制器
if (pos === 100) { // 在尾端
this.movePoint.setCoordinates(this.options.coordinates[0]) // 设置移动点当前位置
this.movePointMarker.setGeometry(this.movePoint)
this.distance = 0
this.currentIndex = 0
// this.redrawMovePoing(this.options.coordinates[0], this.options.coordinates) // 设置移动点到一个点位
}
this.setAnimation(e)
}, (e) => {
this.setAnimationSpeed(e)
})
this.map.addControl(this.playbarControl)
源码
1、 目录结构
2、config.js 默认配置项源码
import IconNav from '@/assets/images/map/marker-icon/icon-nav.png'
/**
* 轨迹默认配置项
* config:{
* type: 'TrajectoryLayer',
layerProperties:{ // 图层属性配置
name:'TrajectoryLayer',
id:'TrajectoryLayer'
}||undefined // layer 属性
coordinates: [], // 道路点位坐标
layerStyle: [],// 定位点样式
action: [{ // marks 事件数组,目前仅支持click 模式
type: 'click',
hitTolerance: 10, // 命中容差
handle: this.handleMarkHover // 事件回调函数
}]
*/
export const defaultConfig = {
type: 'TrajectoryLayer',
layerProperties: {
name: 'TrajectoryLayer',
id: 'TrajectoryLayer'
},
coordinates: [], // 道路点位坐标
layerStyle: [], // 样式配置
actions: [], // 事件配置
lineStyle: { // 道路样式
width: 5,
color: '#FF0C00'
},
navStyle: { // 导航图片样式
scale: 1,
url: IconNav
},
stopStyle: { // 经停点样式
radius: 8,
color: '#ff8f00'
},
speed: 80,
tolerance: 10, // 容差,最小10
imageAngle: 1.5 // 图片自身旋转角度
}
2、自定义控制器源码
import { Control } from 'ol/control'
/**
* 播放控件
*/
export class PlaybarControl extends Control {
/**
* @param {Object} [opt_options] Control options.
*/
constructor (_map, _options, _playCallBack, _speedSetCallBack) {
const options = _options // 配置项
let playContainer = document.createElement('div')
playContainer.id = 'ol-play-control'
playContainer.className = 'ol-play-control'
super({
element: playContainer,
target: options.target
})
this.currentPos = 0
this.palyStatus = false // 默认暂停
this.currentSpeed = 1
this.progressEle = null // 进度条
this.progressTipContianerEle = null // 提示框
this.playOptEle = null // 暂停、开始按钮
this.initPlay(playContainer)
this.playCallBack = _playCallBack
this.speedSetCallBack = _speedSetCallBack
// this.actionOptions = actionOption
}
setProgressEleWidth (pos) {
if (this.progressEle && this.progressTipContianerEle) {
this.currentPos = pos
if (!this.palyStatus) {
this.palyStatus = true
this.playOptEle.className = `${this.palyStatus ? 'ol-opt-playing ol-play-opt' : 'ol-play-opt'}`
// console.log('状态是false 时,this.palyStatus', this.palyStatus, pos, Math.round(pos))
}
// console.log('this.palyStatus', this.palyStatus)
if (this.currentPos > 0 && Math.round(pos) < 100) {
this.progressEle.style.width = this.currentPos + '%'
} else if (this.currentPos === 0 || this.currentPos < 0) {
this.progressEle.style.width = '0px'
} else if (Math.round(pos) === 100 || Math.round(pos) > 100) {
this.currentPos = 100
this.progressEle.style.width = '100%'
this.palyStatus = false
this.playOptEle.className = `${this.palyStatus ? 'ol-opt-playing ol-play-opt' : 'ol-play-opt'}`
// console.log('进入', this.palyStatus)
}
if (this.currentPos === 0 || this.currentPos < 0) {
this.progressTipContianerEle.style.display = 'none'
} else {
this.progressTipContianerEle.style.display = 'block'
let width = this.progressTipContianerEle.clientWidth
// console.log('width=', width)
this.progressTipContianerEle.style.left = width / 2 + 'px'
// if (this.currentPos > 0 && this.currentPos < 100) {
// this.progressTipContianerEle.style.left = width / 2 + 'px'
// } else if (this.currentPos === 0 || this.currentPos < 0) {
// this.progressTipContianerEle.style.left = '100px'
// } else if (Math.round(pos) === 100 || Math.round(pos) > 100) {
// this.currentPos = 100
// this.progressEle.stylestyle.left = width + 'px'
// this.palyStatus = false
// }
}
}
}
/**
* 播放
*/
initPlay (playContainer) {
this.initPlayOptsContainer(playContainer)
this.initProgressContainer(playContainer)
this.initSpeedContainer(playContainer)
}
/**
* 初始化播放、暂停box
*/
initPlayOptsContainer (playContainer) {
this.playOptEle = document.createElement('div') // 开启、停止按钮
this.playOptEle.className = `${this.palyStatus ? 'ol-opt-playing ol-play-opt' : 'ol-play-opt'}`
playContainer.appendChild(this.playOptEle)
this.playOptEle.addEventListener('click', (e) => {
// console.log('ss')
this.palyStatus = !this.palyStatus
this.playOptEle.className = `${this.palyStatus ? 'ol-opt-playing ol-play-opt' : 'ol-play-opt'}`
// console.log('this.onPlay', this.playCall)
this.playCallBack(this.palyStatus, this.currentPos)
})
}
/**
* 初始化进度条容器box
* @param {*} playContainer
*/
initProgressContainer (playContainer) {
let progressContainerEle = document.createElement('div') // 进度条box容器
progressContainerEle.className = 'ol-progress-container'
this.progressEle = document.createElement('div') // 进度条容器
this.progressEle.className = 'ol-progress'
progressContainerEle.appendChild(this.progressEle) // 将进度条容器 写入 进度条box容器
this.progressTipContianerEle = document.createElement('div') // 进度提示容器
this.progressTipContianerEle.className = 'ol-progress-tip-container'
this.progressTipContianerEle.innerText = '当前速度是'
let speedTipEle = document.createElement('div') // 速度容器
speedTipEle.className = 'ol-progress-tip-speed'
speedTipEle.innerText = '0km/h'
this.progressEle.appendChild(this.progressTipContianerEle) // 将速度文字提示ele 写入 进度条
this.progressTipContianerEle.appendChild(speedTipEle) // 将速度容器ele 写入 进度条
playContainer.appendChild(progressContainerEle) // 进度条box容器 play控制器
// setTimeout(() => {
// let width = this.progressTipContianerEle.clientWidth
// this.progressTipContianerEle.style.left = width / 2 + 'px'
// console.log('width', width)
// }, 100)
}
/**
* 初始化速度设置容器box
* @param {*} playContainer
*/
initSpeedContainer (playContainer) {
let playSpeed = document.createElement('select') // 播放速度样式
playSpeed.className = 'ol-speed'
playSpeed.options.add(new Option('1x', 1))// 兼容所有浏览器
playSpeed.options.add(new Option('1.5x', 1.5))// 兼容所有浏览器
playSpeed.options.add(new Option('2x', 2))// 兼容所有浏览器
playContainer.appendChild(playSpeed)
playSpeed.addEventListener('change', (e) => {
// console.log('选择 e', e, e.target.value)
this.speedSetCallBack(e.target.value)
})
}
}
3、全局添加控制器样式
#ol-play-control{
padding:10px 15px;
background: rgba(0,0,0,.5);
color:#fff;
position: absolute;
bottom: 10px;
right:10px;
border-radius: 10px;
left: 10px;
@include flexLayout($vertical:center);
font-size: 12px;
.ol-play-opt{
position: relative;
width: 30px;
height: 30px;
cursor: pointer;
&::before{
content:'';
position: absolute;
top:5px;
left:9px;
border-style: solid;
border-color: transparent;
border-width: 12px 0 12px 12px;
border-left-color: #fff;
}
&.ol-opt-playing{
&::before,&::after{
content:'';
position: absolute;
width:4px;
// height: 10px;
top:5px;
bottom:5px;
background: #fff;
border-radius: 5px;
}
&::before{
left:8px;
border:0
}
&::after{
right:8px
}
}
}
.ol-progress-container{
flex:1;
margin:0 5px;
background: rgba(#fff,.5);
height: 5px;
position: relative;
.ol-progress{
height: 100%;
background: #fff;
width: 0%;
position: relative;
@include flexLayout($vertical:center,$horizontal:flex-end);
.ol-progress-tip-container{
display: none;
top:-30px;
left:65px;
width:auto;
min-width: 130px;
position: relative;
// background: #fff;
padding:10px;
// padding-left: 0;
background-color: #fff;
// border-radius: 5px;
color:#000;
font-weight: bold;
border-radius: 8px;
border-radius: 8px;
box-shadow: 0 0 5px rgba(#000,.4);
.ol-progress-tip-speed{
display: inline-block;
color:#4969DC;
margin-left:10px;
}
&::after{
content:"";
position: absolute;
bottom:-22px;
left:50%;
margin-left: -12px;
border-style: solid;
border-color: transparent;
border-width: 12px;
border-top-color: #fff;
}
}
}
}
.ol-speed{
border:1px solid rgba(#fff,.8);
background-color: transparent;
color:#fff;
cursor: pointer;
border-radius: 5px;
margin-left: 5px;
text-align: center;
padding:2px 0px;
font-size:14px;
position: relative;
option{
color:#000;
border-color:rgba(#000,.4)
}
}
}
4、轨迹类封装
import * as ol from '../openLayer'
import { getVectorContext } from 'ol/render.js'
import { getDistance } from 'ol/sphere.js'
import { PlaybarControl } from './play'
import { defaultConfig } from './config'
import { deepAssign } from '@/utils'
import { LineString } from 'ol/geom'
/**
* 轨迹类
*/
export default class OlMapTrajectory {
constructor (_map, _config) {
this.map = _map
this.options = deepAssign(defaultConfig, _config) // 配置项
this.line = null // 直线
this.lineFeature = null // 直线feature
this.startMarker = null // 开始点位
this.endMarker = null // 结束点位
this.stopMakers = [] // 经停点
this.routeStyle = null // 轨迹样式
this.movePoint = null // 移动点
this.movePointMarker = null // 移动点feature
this.moveFeature = null // 移动feature
this.lastTime = null // 最后一次移动时间
this.animating = false // 是否正在移动,false-> 停止 true-> 移动
this.trajectoryLayer = null // openlayer 对象
this.speed = this.options.speed // 移动速度
this.distance = 0 // 已经移动的距离
this.currentIndex = 0 // 当前坐标点下标
this.lineAllLength = 0 // 轨迹总长度
this.lineSegmentLength = [] // 线段直接的距离
this.vectorContext = null // canvas 上下文
this.tolerance = this.options.tolerance < 10 ? 10 : this.options.tolerance // 容差,最小为10
this.imageAngle = this.options.imageAngle // 图片自身旋转角度
if (this.options.coordinates.length > 0) {
this.setRouteStyle() // 设置轨迹样式
this.drwaRoute() // 绘制线条
this.initMoveFeature() // 初始化移动moveFeature
this.lineAllLength = this.getLineAllLength() // 获取线条总长度
this.playbarControl = new PlaybarControl(this.map, _config, (e, pos) => { // 添加播放停止动画控制器
if (pos === 100) { // 在尾端
this.movePoint.setCoordinates(this.options.coordinates[0]) // 设置移动点当前位置
this.movePointMarker.setGeometry(this.movePoint)
this.distance = 0
this.currentIndex = 0
// this.redrawMovePoing(this.options.coordinates[0], this.options.coordinates) // 设置移动点到一个点位
}
this.setAnimation(e)
}, (e) => {
this.setAnimationSpeed(e)
})
this.map.addControl(this.playbarControl)
}
}
/**
* 重新绘制移动点位置和样式
*/
redrawMovePoing (currentCoordinate, coordinates, vectorContext) {
this.movePoint.setCoordinates(currentCoordinate) // 设置移动点当前位置
// console.log('this.currentIndex=', this.currentIndex, this.getRotation(coordinates[this.currentIndex], coordinates[this.currentIndex + 1]))
let rotation = this.getRotation(coordinates[this.currentIndex], coordinates[this.currentIndex + 1])
if (rotation > 3) {
rotation = rotation - this.imageAngle
}
let movePointStyle = new ol.Style({
image: new ol.Icon({
src: this.options.navStyle.url,
scale: this.options.navStyle.scale,
rotateWithView: false,
rotation: rotation
})
})
this.vectorContext.setStyle(movePointStyle) // 移动时重新计算线路角度
this.vectorContext.drawGeometry(this.movePoint)
}
/**
* 得到总线路长度
* @returns
*/
getLineAllLength () {
let coordinates = this.options.coordinates
let length = 0
for (let i = 0; i < coordinates.length - 1; i++) {
// console.log(coordinates[i], i)
length = length + this.formatLength(coordinates[i], coordinates[i + 1])
this.lineSegmentLength.push(parseInt(length))
}
console.log('this.lineSegmentLength', this.lineSegmentLength)
return length
}
/**
* 设置动画的速度
* @param {*} e
*/
setAnimationSpeed (e) {
this.speed = e * this.options.speed
// this.initMoveFeature()
}
/**
* 开启或者停止动画
* @param {*} e
*/
setAnimation (e) {
// console.log('e', e)
this.animating = e
if (this.animating) {
this.startAnimation()
} else {
this.stopAnimation()
}
}
/**
* 开始动画
*/
startAnimation () {
console.log('开始移动')
this.animating = true
this.lastTime = new Date().getTime() /** 开始时的时间 */
this.trajectoryLayer.on('postrender', this.moveFeature)
// hide geoMarker and trigger map render through change event
this.movePointMarker.setGeometry(null)
}
/**
* 停止动画
*/
stopAnimation () {
this.animating = false
// Keep marker at current animation position
this.movePointMarker.setGeometry(this.movePoint)
this.trajectoryLayer.un('postrender', this.moveFeature)
}
/**
* 获取线段长度
* @param {*} line
* @returns
*/
formatLength (sourcePoint, targetPoint) {
const length = getDistance(sourcePoint, targetPoint)
let output
if (length > 1000) {
output = Math.round((length / 1000) * 100) / 100 + ' km'
} else {
output = Math.round(length * 100) / 100 + ' m'
}
return getDistance(sourcePoint, targetPoint)
};
/**
* 根据坐标获取角度数,以正上方为0度作为参照
* @param sourcePoint 源点
* @param targetPoint 目标点
*/
getRotation (sourcePoint, targetPoint) {
try {
return -Math.atan2(targetPoint[1] - sourcePoint[1], targetPoint[0] - sourcePoint[0]) + this.imageAngle // 计算导航图标旋转角度
} catch (error) {
console.log(error, sourcePoint, targetPoint)
}
}
/**
* 初始化移动
*/
initMoveFeature () {
if (this.moveFeature) {
console.log('已存在', this.moveFeature)
} else {
let routeLength = this.options.coordinates.length
let coordinates = this.options.coordinates
let moveLength = 0
let preCoordinate = this.options.coordinates[0]
this.moveFeature = (e) => {
// console.log('moveFeaturee=', e)
this.vectorContext = getVectorContext(e) // //HTML5 Canvas context,ol.render.canvas.Immediate的对象
const speed = parseInt(this.speed)
let frameState = e.frameState // freme 的状态
const time = frameState.time
const elapsedTime = time - this.lastTime
if (this.distance === 0) {
preCoordinate = this.options.coordinates[0]
moveLength = 0
this.lastTime = new Date().getTime()
}
// console.log('frameStateindex=', frameState.index)
this.distance = (this.distance + (speed * elapsedTime) / 1e6) % 2
// console.log('this.currentIndex=', this.currentIndex)
// console.log('distance', distance)
this.lastTime = time
const currentCoordinate = this.line.getCoordinateAt(
this.distance > 1 ? 2 - this.distance : this.distance
)
moveLength = moveLength + this.formatLength(preCoordinate, currentCoordinate) // 计算移动过的距离
preCoordinate = currentCoordinate // 缓存上一个定位点
// console.log('moveLength=', currentCoordinate, moveLength)
// console.log('moveLength=', moveLength, this.lineSegmentLength.indexOf(moveLength))
let progressWidth = (moveLength / this.lineAllLength) * 100 // 计算计算进度条的位置
// console.log('progressWidth=', progressWidth)
this.playbarControl.setProgressEleWidth(progressWidth) // 设置进度条的位置
this.getCurrentIndex(moveLength, coordinates) // 计算移动点在坐标数组里的下标
this.redrawMovePoing(currentCoordinate, coordinates) // 从新绘制移动点的位置
this.map.render() // 继续动画效果
if (this.distance > 1) {
this.stopAnimation()
}
}
}
}
/**
* 计算当前坐标点下标,
* moveLength
*/
getCurrentIndex (moveLength, coordinates) {
let filtCoordinate = this.lineSegmentLength.filter(item => {
let tempMoveLength = parseInt(moveLength)
let tempItem
// console.log(tempMoveLength, item)
if (tempMoveLength > item - this.tolerance && tempMoveLength < item + this.tolerance) {
tempItem = item
}
return tempItem
})
// console.log('filtCoordinate=', filtCoordinate)
if (filtCoordinate.length > 0) {
this.currentIndex = this.lineSegmentLength.indexOf(filtCoordinate[0])
// console.log('this.currentIndex=', this.currentIndex)
}
if (this.currentIndex >= coordinates.length) {
this.currentIndex = coordinates.length - 1
}
}
/**
* 道路轨迹
*/
drwaRoute () {
this.line = new LineString(this.options.coordinates) // 直线
// 线路所有坐标点坐标
let coordinates = this.options.coordinates
this.lineFeature = new ol.Feature({ // 直线线路
type: 'line',
geometry: this.line
})
this.startMarker = new ol.Feature({ // 开始点位
type: 'icon',
geometry: new ol.Point(this.line.getFirstCoordinate())
})
this.endMarker = new ol.Feature({ // 结束点位
type: 'icon',
geometry: new ol.Point(this.line.getLastCoordinate())
})
/**
* 经停点
*/
for (let i = 0; i < coordinates.length; i++) {
let s = new ol.Feature({
type: 'stop',
geometry: new ol.Point(coordinates[i])
})
this.stopMakers.push(s)
}
this.movePoint = this.startMarker.getGeometry().clone() // 移动点
this.movePointMarker = new ol.Feature({ // 移动点feature
type: 'geoMarker',
geometry: this.movePoint
})
let routeFeature = [...[this.lineFeature, this.movePointMarker, this.startMarker, this.endMarker], ...this.stopMakers]
this.trajectoryLayer = new ol.VectorLayer({ // 添加图层
source: new ol.VectorSource({
features: routeFeature,
className: 'TrajectoryLayer',
properties: this.options.layerProperties
}),
style: (feature) => {
return this.routeStyle[feature.get('type')]
}
})
this.map.addLayer(this.trajectoryLayer)
}
/**
* 设置轨迹线路样式
*/
setRouteStyle () {
let coordinates = this.options.coordinates
let sourcePoint = coordinates[this.currentIndex]
let targetPoint = coordinates[this.currentIndex + 1]
this.routeStyle = { // 定义线路样式
line: new ol.Style({
stroke: new ol.Stroke({
width: this.options.lineStyle.width,
color: this.options.lineStyle.color
})
}),
geoMarker: new ol.Style({
image: new ol.Icon({
src: this.options.navStyle.url,
scale: this.options.navStyle.scale,
rotateWithView: false,
rotation: this.getRotation(sourcePoint, targetPoint)
})
}),
stop: new ol.Style({
image: new ol.CircleStyle({
radius: this.options.stopStyle.radius - 2,
fill: new ol.Fill({
// width: this.options.routeStyle.width,
color: this.options.stopStyle.color
}),
stroke: new ol.Stroke({
width: (this.options.stopStyle.radius - 2) / 2,
color: this.options.stopStyle.color
})
})
})
}
}
updateTrajectory (_map, _config) {
this.map = _map
this.options = deepAssign(defaultConfig, _config) // 配置项
}
}