前言
在高德地图中,如果使用过导航就可以看到它的轨迹线做的非常漂亮,用openlayer能实现吗?是可以的,只是稍微复杂了一点点。
1. 轨迹线构成分析
我们来看看轨迹线是怎样组成的呢。
2. 使用openlayer实现类高德轨迹线
起点、终点、文字都可以使用openlayer自带的api实现,不复杂,这里就不讨论了。本次重点看下如何实现轨迹线的样式,以及在这个基础上实现箭头的动画效果。
2.1 如何实现一个高度定制的箭头
如果我们要封装实现一个通用的轨迹绘制方法,需要考虑到箭头的问题(大小、箭头的宽度、箭头的角度、颜色可调),箭头不能使用普通的图片。在openlayer中样式类ol.style.Icon中有img属性可以传入HTMLCanvasElement,所以我们可以传入一个canvas对象,这样就简单了,我们可以根据传入的参数来定制这个箭头
第二个问题是我们如何计算出箭头的样式的各个顶点。我们可以根据箭头的宽高,使用反三角函数来依次计算出各个顶点。
/**
* 获取航线右箭头
* @param param
* @returns {HTMLCanvasElement}
*/
function getRouteCanvasArrow(param) {
let opt = Object.assign(
{
color: 'rgba(108,159,67,1)',
lineWidth: 4,
arrowHeight: 28,
// 箭头夹角(度)
angle: 110
},
param
);
if (opt.angle >= 180 || opt.angle <= 0) {
throw new Error('箭头夹角取值范围为0-180');
}
const singleHeight = opt.arrowHeight / 2;
// 计算出箭头宽度
// 计算余切值
const cotA = 1 / Math.tan((opt.angle * Math.PI) / 360);
// 使用余切值计算相邻直角边, 箭头偏移宽度
const offsetWidth = Math.ceil(cotA * singleHeight);
const arrowWidth = offsetWidth + opt.lineWidth;
const canvas = document.createElement('canvas');
canvas.width = arrowWidth;
canvas.height = opt.arrowHeight;
let ctx = canvas.getContext('2d');
ctx.fillStyle = opt.color;
ctx.strokeStyle = opt.color;
ctx.beginPath();
ctx.lineTo(0, 0);
ctx.lineTo(opt.lineWidth, 0);
ctx.lineTo(arrowWidth, singleHeight);
ctx.lineTo(opt.lineWidth, opt.arrowHeight);
ctx.lineTo(0, opt.arrowHeight);
ctx.lineTo(offsetWidth, singleHeight);
ctx.closePath();
ctx.stroke();
ctx.fill();
return canvas;
}
2.2 实现轨迹线样式
2.2.1 箭头间隔如何计算
我们可以使用ol.geom.LineString的getCoordinateAt方法来获取线上的点(参数取值范围0-1,0是起点,1是终点)。同时我们可以获取当前地图分辨率resolution,通过线的geometry的长度除以分辨率resulotion(lineGeom.getLength() / resolution ),可以得到线的总长度像素。假如我们需要箭头间隔是50个像素,我们只需要用50除以线段的总长度就知道需要几个箭头了,然后用得到的百分比调用上面提到的ol.geom.LineString的getCoordinateAt方法获取箭头的位置。
2.2.2 如何实现箭头动画
在openlayer里可以监听图层的postcompose事件,拿到地图的framestate和vectorContext(vectorContext可以使用来绘制feature,性能比较高),如果要执行动画,只需要把framestate的animate属性设置为true即可,这样地图自己会去判断,并且使用requestAnimationFrame去执行map.render绘制下一帧,达到连续绘制形成动画的效果。
如何让箭头像前进一样动起来呢,只需要在每次postcompose事件里,我们自己绘制时改变一下箭头的位置,让其向前移动一点点(比如说1个像素),就可以了。
2.2.3 封装绘制高德轨迹样式的函数
按照上面的思路,封装绘制函数,参数可以输入线的宽度,颜色,箭头样式,间隔,是否开启箭头动画等参数。除了必要参数,都可以使用默认参数,就可以绘制出来,使用起来比较方便。
import * as MapUtil from './MapUtil';
import _ from 'lodash';
// 用于判断feature是否存在
let routeUniqId = 0;
/**
* 绘制类似高德、百度的轨迹样式
* @param { String | ol.vector.Layer } param.lineLayer 【必填】图层名或图层对象
* @param { WKTString | ol.geom.LineString } param.lineGeom 【必填】轨迹线
* @param { String } param.theme 【选填】设置样式,可选值:gray(类似于走过的灰色)
* @param { Array<Integer> } param.lineColor 【选填】线颜色
* @param { number } param.lineWidth 【选填】线宽(px)
* @param { Array<Integer> | boolean } param.borderColor 【选填】线边框颜色,为false时不绘制边框
* @param { number } param.borderWidth 【选填】线边框宽度(px)
* @param { Array<Integer> | boolean } param.arrowColor 【选填】箭头颜色,为false时不绘制箭头
* @param { number } param.arrowWidth 【选填】箭头线宽度(px)
* @param { number } param.arrowGap 【选填】箭头间距(px)
* @param { boolean } param.arrowAnime 【选填】是否开启箭头动画,默认否
* @param { (feature: ol.Feature) => void } param.cb 【选填】回调函数
*/
function drawRouteLine(param) {
let opt = Object.assign(
{
lineLayer: null,
lineGeom: null,
theme: false,
lineColor: [0, 186, 107],
lineWidth: 10,
// 为false时不绘制边框
borderColor: [4, 110, 74],
borderWidth: 2,
// 为false时不绘制箭头
arrowColor: 'rgba(255, 255, 255, 1)',
arrowWidth: 3,
arrowGap: 100,
// 箭头动画
arrowAnime: false,
cb: null
},
param
);
let lineLayer = _.isString(opt.lineLayer) ? MapUtil.getLayer(opt.lineLayer) : opt.lineLayer;
let lineGeom = _.isString(opt.lineGeom) ? MapUtil.WKT.readGeometry(opt.lineGeom) : opt.lineGeom;
// 1 预处理theme
if (opt.theme === 'gray') {
opt.lineColor = [154, 207, 197];
opt.borderColor = [127, 174, 159];
}
// 2.1 线样式
let bottomPathStyle;
let upperPathStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
color: opt.lineColor,
width: opt.lineWidth
})
});
// 2.2 获取canvas箭头
let arrowCanvas = MapUtil.getRouteCanvasArrow({
color: opt.arrowColor,
lineWidth: opt.arrowWidth,
arrowHeight: opt.lineWidth
});
// 2.3 获取箭头样式
const createArrowStyle = function (resolution, offset, vectorContext) {
let resStyles = [];
let lineLength = lineGeom.getLength() / resolution;
if (lineLength < 50) {
return resStyles;
}
let numArr = Math.ceil(lineLength / opt.arrowGap);
let points = [];
for (let i = 0; i <= numArr; i++) {
let fracPos = i / numArr + offset;
if (fracPos > 1) fracPos -= 1;
let pg = new ol.Feature({
geometry: new ol.geom.Point(lineGeom.getCoordinateAt(fracPos))
});
points.push(pg);
}
//确定方向并绘制
lineGeom.forEachSegment((start, end) => {
let line = new ol.geom.LineString([start, end]);
_.forEach(points, point => {
let coord = point.getGeometry().getFirstCoordinate();
let cPoint = line.getClosestPoint(coord);
if (Math.abs(cPoint[0] - coord[0]) < 1 && Math.abs(cPoint[1] - coord[1]) < 1) {
let dx = end[0] - start[0];
let dy = end[1] - start[1];
let rotation = Math.atan2(dy, dx);
let arrowStyle = new ol.style.Style({
image: new ol.style.Icon({
img: arrowCanvas,
anchor: [1, 0.5],
rotateWithView: true,
rotation: -rotation,
imgSize: [arrowCanvas.width, arrowCanvas.height]
})
});
if (vectorContext) {
vectorContext.drawFeature(point, arrowStyle);
} else {
arrowStyle.setGeometry(point.getGeometry());
resStyles.push(arrowStyle);
}
}
});
});
return resStyles;
};
// 3.绘制线
if (opt.borderColor) {
bottomPathStyle = new ol.style.Style({
stroke: new ol.style.Stroke({
color: opt.borderColor,
width: opt.lineWidth + 2 * opt.borderWidth
})
});
MapUtil.drawVectorFeature(lineLayer, {
geom: lineGeom,
style: bottomPathStyle
});
}
let uid = routeUniqId++;
let styles = upperPathStyle;
if (!opt.arrowAnime && opt.arrowColor) {
styles = function (feature, resolution) {
let lineLength = lineGeom.getLength() / resolution;
let offset = opt.arrowGap / lineLength / 2;
let res = createArrowStyle(resolution, offset);
res.push(upperPathStyle);
return res;
};
}
let lf = MapUtil.drawVectorFeature(lineLayer, {
geom: lineGeom,
style: styles,
kv: { _uid_: uid }
});
// 4 执行回调
try {
opt.cb && opt.cb(lf);
} catch (e) {
console.error(e);
}
// 5.1 绘制箭头动画
if (!opt.arrowAnime || !opt.arrowColor) {
return;
}
// 5.2 绘制箭头动画函数
let offset = 0.01;
const drawArrow = function (evt) {
let features = MapUtil.getFeatureByFilter(lineLayer, f => f.get('_uid_') == uid);
if (!features.length) {
lineLayer.un('postcompose', drawArrow);
return;
}
let resolution = evt.frameState.viewState.resolution;
createArrowStyle(resolution, offset, evt.vectorContext);
let lineLength = lineGeom.getLength() / resolution;
offset = offset + 1.5 / lineLength;
//复位
if (offset >= 1) offset = 0.001;
// 告诉地图执行动画
evt.frameState.animate = true;
};
lineLayer.on('postcompose', drawArrow);
}
2.3 最终效果
使用封装的函数绘制出来的轨迹线,效果还是比较不错的。