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

如何在OpenLayers中按文本框输入值添加动画箭头

问题:在LineString路线上按输入数量逐个添加动画箭头

需求:在文本框输入数字(例如4),点击「添加箭头」按钮后,每隔2秒在路线上添加一个动画箭头,最终添加的箭头数量与输入值一致。


现有实现代码

HTML代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Marker Animation</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <label for="speed">
      speed:&nbsp;
      <input id="speed" type="range" min="10" max="999" step="10" value="60">
    </label>
    <button id="start-animation">Start Animation</button>
    <input type="text" id="textValue">
    <button id="btnClick">Add icon</button>
    <!-- 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(),
    }),
  ],
});

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 position = startMarker.getGeometry().clone();
    const geoMarker = new Feature({
      type: 'geoMarker',
      geometry: position,
    });

    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',
          rotation: getAngleAt(route, 0) + Math.PI / 2,
        }),
      }),
    };

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

    map.addLayer(vectorLayer);

    const speedInput = document.getElementById('speed');
    const startButton = document.getElementById('start-animation');
    let animating = false;

    function getAngleAt(lineString, distance) {
      const length = lineString.getLength();
      const coordinates = lineString.getCoordinates();
      for (let i = 1, len = coordinates.length; i <= len; ++i) {
        if (
          new LineString(coordinates.slice(0, i + 1)).getLength() >=
          length * distance
        ) {
          return -Math.atan2(
            coordinates[i][1] - coordinates[i - 1][1],
            coordinates[i][0] - coordinates[i - 1][0]
          );
        }
      }
    }

    const lastTimes = [];
    function moveFeature(event) {
      const speed = Number(speedInput.value);
      const time = event.frameState.time;
      for (let i = 0, ii = lastTimes.length; i < ii; ++i) {
        let {lastTime, distance, position} = lastTimes[i];
        const elapsedTime = time - lastTime;
        distance = (distance + (speed * elapsedTime) / 1e6) % 2;
        lastTime = time;
        lastTimes[i] = {lastTime, distance, position};
        const lineDistance = distance > 1 ? 2 - distance : distance;
        const direction = distance > 1 ? -Math.PI / 2 : Math.PI / 2;
        const currentCoordinate = route.getCoordinateAt(lineDistance);
        const angle = getAngleAt(route, lineDistance) + direction;
        styles.geoMarker.getImage().setRotation(angle);
        position.setCoordinates(currentCoordinate);
        const vectorContext = getVectorContext(event);
        vectorContext.setStyle(styles.geoMarker);
        vectorContext.drawGeometry(position);
      }
      map.render();
    }

    function startAnimation() {
      lastTimes.push({lastTime: Date.now(), distance: 0, position: startMarker.getGeometry().clone()});
      if (!animating) {
        animating = true;
        vectorLayer.on('postrender', moveFeature);
        geoMarker.setGeometry(null);
      }
    }

    startButton.addEventListener('click', startAnimation);

    // 新增:添加箭头按钮逻辑
    const addBtn = document.getElementById('btnClick');
    const countInput = document.getElementById('textValue');
    addBtn.addEventListener('click', function() {
      const count = parseInt(countInput.value);
      if (isNaN(count) || count <= 0) return;
      
      let addedCount = 0;
      const interval = setInterval(() => {
        if (addedCount >= count) {
          clearInterval(interval);
          return;
        }
        startAnimation();
        addedCount++;
      }, 2000);
    });
  });
});

关键修改说明

  1. 独立维护箭头状态:修改lastTimes数组,每个元素包含独立的position对象,避免多个箭头共用同一位置导致的覆盖问题。
  2. 定时添加逻辑:为「添加箭头」按钮绑定点击事件,读取输入框数值后,通过setInterval每隔2秒调用startAnimation添加新箭头,达到指定数量后清除定时器。
  3. 更新动画绘制:moveFeature函数遍历所有箭头状态,分别计算每个箭头的位置和角度并绘制,确保每个箭头独立运动。

内容的提问来源于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 08:40:35