You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在OpenLayers的单条线串上添加多个动画箭头图标

OpenLayers实现单条LineString多动画箭头图标

需要在单条LineString路径上实现点击「Add Animation Arrow」按钮添加一个动画箭头,再次点击新增相同动画箭头的效果,原代码仅支持单个箭头,以下是修改后的完整实现方案:


修改后的HTML代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Multi Marker Animation</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
      .controls {
        margin: 10px 0;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <div class="controls">
      <label for="speed">
        speed:&nbsp;
        <input id="speed" type="range" min="10" max="999" step="10" value="60">
      </label>
      <button id="start-animation">Add Animation Arrow</button>
      <button id="stop-all">Stop All Animations</button>
    </div>
    <!-- Pointer events polyfill for old browsers -->
    <script src="https://unpkg.com/elm-pep@1.0.6/dist/elm-pep.js"></script>
    <script type="module" src="main.js"></script>
  </body>
</html>

修改后的JavaScript代码

import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import Polyline from 'ol/format/Polyline';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import OSM from 'ol/source/OSM';
import {Icon, Stroke, Style} from 'ol/style';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {getVectorContext} from 'ol/render';

const center = [-5639523.95, -3501274.52];
const map = new Map({
  target: document.getElementById('map'),
  view: new View({
    center: center,
    zoom: 10,
    minZoom: 2,
    maxZoom: 19,
  }),
  layers: [
    new TileLayer({
      source: new OSM(),
    }),
  ],
});

// 存储所有动画箭头的状态
let animationMarkers = [];
let animating = false;

// 加载路径数据
fetch('data/polyline/route.json').then(function (response) {
  response.json().then(function (result) {
    const polyline = result.routes[0].geometry;

    const route = new Polyline({
      factor: 1e6,
    }).readGeometry(polyline, {
      dataProjection: 'EPSG:4326',
      featureProjection: 'EPSG:3857',
    });

    const routeFeature = new Feature({
      type: 'route',
      geometry: route,
    });
    const startMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getFirstCoordinate()),
    });
    const endMarker = new Feature({
      type: 'icon',
      geometry: new Point(route.getLastCoordinate()),
    });

    const styles = {
      route: new Style({
        stroke: new Stroke({
          width: 6,
          color: [237, 212, 0, 0.8],
        }),
      }),
      icon: new Style({
        image: new Icon({
          anchor: [0.5, 1],
          src: 'data/icon.png',
        }),
      }),
      geoMarker: new Style({
        image: new Icon({
          src: 'https://cdn1.iconfinder.com/data/icons/basic-ui-elements-color-round/3/19-32.png',
        }),
      }),
    };

    const vectorLayer = new VectorLayer({
      source: new VectorSource({
        features: [routeFeature, startMarker, endMarker],
      }),
      style: function (feature) {
        return styles[feature.get('type')];
      },
    });

    map.addLayer(vectorLayer);

    const speedInput = document.getElementById('speed');
    const startButton = document.getElementById('start-animation');
    const stopAllButton = document.getElementById('stop-all');

    // 计算路径指定比例位置的方向角
    function getAngleAt(lineString, distance) {
      const length = lineString.getLength();
      const targetDistance = length * distance;
      let currentLength = 0;
      const coordinates = lineString.getCoordinates();
      
      for (let i = 1; i < coordinates.length; i++) {
        const segmentLength = new LineString([coordinates[i-1], coordinates[i]]).getLength();
        currentLength += segmentLength;
        
        if (currentLength >= targetDistance) {
          const dx = coordinates[i][0] - coordinates[i-1][0];
          const dy = coordinates[i][1] - coordinates[i-1][1];
          return -Math.atan2(dy, dx);
        }
      }
      return 0;
    }

    // 批量更新并绘制所有动画箭头
    function moveFeature(event) {
      const speed = Number(speedInput.value);
      const time = event.frameState.time;
      const vectorContext = getVectorContext(event);

      animationMarkers.forEach(marker => {
        const elapsedTime = time - marker.lastTime;
        marker.distance = (marker.distance + (speed * elapsedTime) / 1e6) % 2;
        marker.lastTime = time;

        const lineDistance = marker.distance > 1 ? 2 - marker.distance : marker.distance;
        const direction = marker.distance > 1 ? -Math.PI / 2 : Math.PI / 2;
        const currentCoordinate = route.getCoordinateAt(lineDistance);
        const angle = getAngleAt(route, lineDistance) + direction;

        // 更新当前箭头的位置和旋转角度
        marker.position.setCoordinates(currentCoordinate);
        const markerStyle = styles.geoMarker.clone();
        markerStyle.getImage().setRotation(angle);
        
        // 绘制箭头
        vectorContext.setStyle(markerStyle);
        vectorContext.drawGeometry(marker.position);
      });

      // 持续触发动画循环
      if (animationMarkers.length > 0) {
        map.render();
      } else {
        stopAllAnimations();
      }
    }

    // 添加新的动画箭头
    function addAnimationMarker() {
      const position = startMarker.getGeometry().clone();
      const newMarker = {
        position: position,
        distance: 0,
        lastTime: Date.now()
      };
      animationMarkers.push(newMarker);

      // 首次添加时启动动画
      if (!animating) {
        animating = true;
        vectorLayer.on('postrender', moveFeature);
      }
    }

    // 停止所有动画并清空箭头
    function stopAllAnimations() {
      animating = false;
      animationMarkers = [];
      vectorLayer.un('postrender', moveFeature);
      map.render();
    }

    // 绑定按钮事件
    startButton.addEventListener('click', addAnimationMarker);
    stopAllButton.addEventListener('click', stopAllAnimations);
  });
});

关键改动说明

  • 多箭头状态管理:用animationMarkers数组存储每个箭头的独立状态(位置、当前距离、更新时间),实现多箭头的并行动画控制
  • 动态添加逻辑:点击按钮时创建新的箭头实例并加入数组,首次添加自动启动动画循环
  • 批量绘制优化:遍历所有箭头实例,分别计算位置和角度,克隆样式避免多个箭头的旋转角度互相覆盖
  • 性能优化:重写getAngleAt函数,减少不必要的LineString创建,提升动画流畅度
  • 交互逻辑调整:将原启停按钮拆分为「添加箭头」和「停止所有」,更贴合需求场景

内容的提问来源于stack exchange,提问作者Sanjay Singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 18:50:23