openLayers 实现导航轨迹动态效果

156 阅读3分钟
  • 效果图(箭头是流动的)

image.png

  • 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 { 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 { Style } from "ol/style";
import Stroke from "ol/style/Stroke.js";
import { isEmpty } from 'ol/extent';

// import { getDestination, lineDistance, pathAngle } from "./utils";
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);
};



const DEFAULT_LINE_WIDTH = 5 * window.devicePixelRatio; /* 轨迹线段默认宽度 */
const DEFAULT_LINE_OUT_WIDTH = DEFAULT_LINE_WIDTH * 1.2;
const DASH_OFFSET = 50 * window.devicePixelRatio;
const DURATION = 30;

let offset = DASH_OFFSET;
let layer = null;
let timer = null;
let map = null;
const initLayer = (data, cMap) => {
    map = cMap;
    delLayer()
    let newData = [];
    data.forEach((item) => {
        newData.push([item.longitude, item.latitude]);
    });
    // newData.splice(newData.length - 1);
    const lineString = new LineString(newData);
    const routeFeature = new Feature({
        geometry: lineString
    });
    // const routeFeature = new Feature(lineString);
    routeFeature.setStyle(
        new Style({
            stroke: new Stroke({
                color: [0, 0, 0, 0],
            })
        })
    );
    const source = new VectorSource({
        features: [routeFeature]
    });
    layer = new VectorLayer({
        source,
    });



    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;
        }
    }

    function getLineOffsetDashPixelCoords (coordinates, offset) {
        const destinationArray = [];
        let marginDistance = 0;
        marginDistance += offset;
        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;
    }

    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, offset);

                ctx.lineCap = "round";
                ctx.lineJoin = "round";
                /* 绘制线段-外层 */
                ctx.lineWidth = DEFAULT_LINE_OUT_WIDTH;
                ctx.strokeStyle = "rgb(156, 0, 34)";
                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();
                    });
            });
        });
    }

    function flash () {
        map.render();
    }
    layer.on("prerender", rendererArrows);
    map.addLayer(layer);

    // const source = layer.getSource();
    // 调整地图可视范围到当前layer
    const extent = source.getExtent();
    // 如果范围有效,调整视图
    if (!isEmpty(extent)) {
        map.getView().fit(extent, {
            padding: [100, 100, 100, 100], // 边距
            duration: 1000
        });
    }
    timer = setInterval(() => {
        if (!(offset % DASH_OFFSET)) {
            offset = DASH_OFFSET;
        }
        offset -= 1;
        flash();
    }, DURATION);
}
// 清除
 const delLayer = () => {
    if (layer) map.removeLayer(layer);
    if (timer) clearInterval(timer);
}
export { initLayer, delLayer };
  • 页面中使用
import { initLayer, delLayer } from './main.js';
initLayer(data, map);