openlayers7实现类似高德路径轨迹效果
基于ol7实现线的轨迹渲染效果,附代码实现
效果如下:
轨迹路径效果
-
高德效果
-
本文实现效果
实现思路
- 创建矢量图层
- 设置矢量图层默认样式,设置默认样式不可见
- 动态更新矢量图层的线样式
- 基于canvas实现的线段轨迹效果
实现代码
- main.js
import Feature from "ol/Feature.js";
import { fromLonLat } from "ol/proj";
import Map from "ol/Map.js";
import VectorSource from "ol/source/Vector.js";
import View from "ol/View.js";
import XYZ from "ol/source/XYZ.js";
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer.js";
import { getVectorContext } from "ol/render.js";
import LineString from "ol/geom/LineString.js";
import MultiLineString from "ol/geom/MultiLineString";
import { transform2D } from "ol/geom/flat/transform";
import { getLineOffsetDashPixelCoords, getDestination } from "./utils";
import { Style } from "ol/style";
import Stroke from "ol/style/Stroke.js";
const DEFAULT_LINE_WIDTH = 10 * window.devicePixelRatio; /* 轨迹线段默认宽度 */
const DEFAULT_LINE_OUT_WIDTH = DEFAULT_LINE_WIDTH * 1.2;
function rendererArrows(event) {
const vectorContext = getVectorContext(event);
const layer = event.target;
const transform = vectorContext["transform_"];
const ctx = vectorContext["context_"];
const source = layer.getSource();
const viewExtent = event.frameState.extent;
source.forEachFeatureInExtent(viewExtent, (feature) => {
const geometry = feature.getGeometry();
let lineStrings;
if (geometry instanceof MultiLineString)
lineStrings = geometry.getLineStrings();
else if (geometry instanceof LineString) lineStrings = [geometry];
if (!lineStrings) return;
lineStrings.forEach((lineString, index) => {
const coords = lineString.getFlatCoordinates();
const pixelCoordinates = transform2D(
coords,
0,
coords.length,
2,
transform,
[]
);
let pixelCoords = [];
for (let j = 0; j < pixelCoordinates.length; j += 2) {
const x = pixelCoordinates[j];
const y = pixelCoordinates[j + 1];
pixelCoords.push([x, y]);
}
const dashPixelCoords = getLineOffsetDashPixelCoords(pixelCoords);
ctx.lineCap = "round";
ctx.lineJoin = "round";
/* 绘制线段-外层 */
ctx.lineWidth = DEFAULT_LINE_OUT_WIDTH;
ctx.strokeStyle = "rgba(0,156,106,1)";
ctx.beginPath();
dashPixelCoords
.filter((dash) => !dash[3])
.forEach((dash, index) => {
const [x, y] = dash;
if (index === 0) ctx.moveTo(dash[0], dash[1]);
ctx.lineTo(x, y);
});
ctx.stroke();
/* 绘制线段-内层 */
ctx.lineWidth = DEFAULT_LINE_WIDTH;
ctx.strokeStyle = "rgba(1,194,125,1)";
ctx.beginPath();
dashPixelCoords
.filter((dash) => !dash[3])
.forEach((dash, index) => {
const [x, y] = dash;
if (index === 0) ctx.moveTo(dash[0], dash[1]);
ctx.lineTo(x, y);
});
ctx.stroke();
/* 绘制箭头 */
ctx.lineWidth = 1.5 * window.devicePixelRatio;
ctx.strokeStyle = "white";
dashPixelCoords
.filter((dash) => dash[3])
.forEach((dash, index) => {
const [x, y, angle] = dash;
/* 反向找到一个点A,用A点与当前点计算箭头另外两个点 */
const arrowCoord = getDestination([x, y], angle - 180, 5);
var l = DEFAULT_LINE_WIDTH * 0.65;
var a = Math.atan2(y - arrowCoord[1], x - arrowCoord[0]);
var x3 = x - l * Math.cos(a + (45 * Math.PI) / 180); // θ=30
var y3 = y - l * Math.sin(a + (45 * Math.PI) / 180);
var x4 = x - l * Math.cos(a - (45 * Math.PI) / 180);
var y4 = y - l * Math.sin(a - (45 * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(x3, y3);
ctx.lineTo(x, y);
ctx.lineTo(x4, y4);
ctx.stroke();
});
});
});
}
const lineString = new LineString([
fromLonLat([89.911906, 41.148991]),
fromLonLat([107.490031, 40.316507]),
fromLonLat([118.124797, 36.918396]),
fromLonLat([112.367961, 24.090737])
]);
const routeFeature = new Feature({
geometry: lineString
});
routeFeature.setStyle(
new Style({
stroke: new Stroke({
color: [0, 0, 0, 0]
})
})
);
const source = new VectorSource({
features: [routeFeature]
});
const layer = new VectorLayer({ source });
layer.on("prerender", rendererArrows);
const map = new Map({
target: document.getElementById("map"),
view: new View({
center: fromLonLat([108, 27]),
zoom: 3,
minZoom: 2,
maxZoom: 19
}),
layers: [
new TileLayer({
source: new XYZ({
url:
"http://webrd0{1-4}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}"
})
}),
layer
]
});
- utils.js
const DASH_OFFSET = 50 * window.devicePixelRatio; /* 间隔,单位屏幕像素 */
export function lineDistance(from, to) {
const [x1, y1] = from;
const [x2, y2] = to;
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
}
/* 根据一个点按夹角、距离计算第二个点,不适用地理坐标系 */
export function getDestination(from, bearing, distance) {
const [x, y] = from;
const endX = Math.cos((bearing * Math.PI) / 180) * distance + x;
const endY = Math.sin((bearing * Math.PI) / 180) * distance + y;
return [endX, endY];
}
export const pathAngle = (points, toDegrees = false) => {
const [
[pointFromX = 0, pointFromY = 0],
[pointToX = 0, pointToY = 0]
] = points;
return toDegrees
? (Math.atan2(pointToY - pointFromY, pointToX - pointFromX) * 180) / Math.PI
: Math.atan2(pointToY - pointFromY, pointToX - pointFromX);
};
function splitLinePoint(from, to, res, marginDistance) {
let _distnace = lineDistance(from, to); /* 线段长度 */
let _bearing = pathAngle([from, to], true); /* 线段夹角 */
const offsetDistance = DASH_OFFSET - marginDistance;
if (_distnace > offsetDistance) {
const [endX, endY] = getDestination(from, _bearing, offsetDistance);
res.push([endX, endY, _bearing, true]);
return splitLinePoint([endX, endY], to, res, 0);
} else {
return _distnace + marginDistance;
}
}
export function getLineOffsetDashPixelCoords(coordinates) {
const destinationArray = [];
let marginDistance = 0;
for (let d = 0, len = coordinates.length - 1; d < len; d++) {
const [from, to] = [coordinates[d], coordinates[d + 1]];
destinationArray.push([...from, 0, false]);
marginDistance = splitLinePoint(from, to, destinationArray, marginDistance);
destinationArray.push([...to, 0, false]);
}
return destinationArray;
}