如何在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: <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
相关产品推荐
相关产品推荐

