Leaflet 1.7.1中自定义带方向圆形标记的实现方法问询
嘿,这个带方向的圆形标记需求我帮你搞定!既然你提到用四点SVG折线的方式简单高效,那我们可以直接基于Leaflet的原生API扩展一个L.DirectedCircle类,不用找插件,直接写JS代码就能实现,而且pyqtlet2也能轻松调用。
实现思路
我们要继承Leaflet的L.Circle类,在原有圆形标记的基础上,添加一个基于折线的方向箭头。核心步骤是:
- 计算方向相关的坐标点(圆心、圆边缘方向点、箭头尖端、箭头两翼)
- 用
L.Polyline绘制四点折线构成箭头 - 把箭头和圆形标记绑定,实现同步添加/移除、样式同步
JavaScript 实现代码
首先写自定义类和辅助函数,直接在Leaflet环境中运行:
// 辅助函数:根据起点、距离(米)、heading角度计算目标经纬度 // heading 0为正北,顺时针递增 function computeDestinationPoint(latLng, distance, heading) { const earthRadius = 6378137; // WGS84地球半径(米) const rad = Math.PI / 180; const lat1 = latLng.lat * rad; const lng1 = latLng.lng * rad; const hdg = heading * rad; // 球面三角计算目标点 const lat2 = Math.asin(Math.sin(lat1) * Math.cos(distance/earthRadius) + Math.cos(lat1) * Math.sin(distance/earthRadius) * Math.cos(hdg)); const lng2 = lng1 + Math.atan2(Math.sin(hdg) * Math.sin(distance/earthRadius) * Math.cos(lat1), Math.cos(distance/earthRadius) - Math.sin(lat1) * Math.sin(lat2)); return L.latLng(lat2 / rad, lng2 / rad); } // 扩展L.Circle,创建带方向的圆形标记 L.DirectedCircle = L.Circle.extend({ // 默认配置,可自定义 options: { heading: 0, // 方向角度(0-360) arrowLength: null, // 箭头长度,默认是半径的0.5倍 arrowWidth: null // 箭头两翼宽度,默认是半径的0.3倍 }, initialize: function(latLng, options) { L.Circle.prototype.initialize.call(this, latLng, options); this._arrow = null; // 存储箭头折线层 }, onAdd: function(map) { // 先绘制基础圆形 L.Circle.prototype.onAdd.call(this, map); // 创建并添加箭头 this._updateArrow(); map.addLayer(this._arrow); }, onRemove: function(map) { // 移除箭头 if (this._arrow) { map.removeLayer(this._arrow); this._arrow = null; } // 移除基础圆形 L.Circle.prototype.onRemove.call(this, map); }, // 更新箭头的位置和形状 _updateArrow: function() { const map = this._map; if (!map) return; const center = this.getLatLng(); const radius = this.getRadius(); const heading = this.options.heading; const arrowLength = this.options.arrowLength || radius * 0.5; const arrowWidth = this.options.arrowWidth || radius * 0.3; // 计算箭头相关的四个关键点位(对应你说的四点折线) const edgePoint = computeDestinationPoint(center, radius, heading); // 圆边缘方向点 const tipPoint = computeDestinationPoint(center, radius + arrowLength, heading); // 箭头尖端 const leftWing = computeDestinationPoint(tipPoint, arrowWidth, heading - 135); // 左翼翼尖 const rightWing = computeDestinationPoint(tipPoint, arrowWidth, heading + 135); // 右翼翼尖 // 四点折线:edgePoint → leftWing → tipPoint → rightWing const arrowLatLngs = [edgePoint, leftWing, tipPoint, rightWing]; if (this._arrow) { this._arrow.setLatLngs(arrowLatLngs); } else { // 箭头样式和圆形保持一致,也可单独自定义 this._arrow = L.polyline(arrowLatLngs, { color: this.options.color, weight: this.options.weight, fill: true, fillColor: this.options.color, fillOpacity: this.options.fillOpacity }); } }, // 新增方法:动态更新方向 setHeading: function(heading) { this.options.heading = heading; this._updateArrow(); return this; }, // 重写setRadius,半径变化时同步更新箭头 setRadius: function(radius) { L.Circle.prototype.setRadius.call(this, radius); this._updateArrow(); return this; } }); // 工厂函数,方便调用(和你期望的L.directedCircle用法一致) L.directedCircle = function(latLng, options) { return new L.DirectedCircle(latLng, options); };
使用示例
// 初始化地图(假设已有map实例) const map = L.map('map').setView([51.505, -0.09], 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); // 创建带方向的圆形标记,和你想要的写法完全一致! const myDirectedCircle = L.directedCircle([51.505, -0.09], { radius: 100, // 圆形半径(米) heading: 45, // 方向:45度(东北) color: 'darkblue', fillColor: 'blue', fillOpacity: 0.6, arrowLength: 60, // 自定义箭头长度 arrowWidth: 30 // 自定义箭头宽度 }).addTo(map); // 动态调整方向或半径 myDirectedCircle.setHeading(90); // 改为正东方向 myDirectedCircle.setRadius(150); // 放大圆形半径
PyQtlet2 适配方案
因为你用pyqtlet2,可以把上述JS代码注入到地图组件中,然后调用工厂函数:
from pyqtlet import L, MapWidget import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout app = QApplication(sys.argv) widget = QWidget() layout = QVBoxLayout(widget) # 创建地图组件 mapWidget = MapWidget() layout.addWidget(mapWidget) leafletMap = L.map(mapWidget) leafletMap.setView([51.505, -0.09], 13) L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(leafletMap) # 注入自定义JS代码 custom_js = """ function computeDestinationPoint(latLng, distance, heading) { const earthRadius = 6378137; const rad = Math.PI / 180; const lat1 = latLng.lat * rad; const lng1 = latLng.lng * rad; const hdg = heading * rad; const lat2 = Math.asin(Math.sin(lat1) * Math.cos(distance/earthRadius) + Math.cos(lat1) * Math.sin(distance/earthRadius) * Math.cos(hdg)); const lng2 = lng1 + Math.atan2(Math.sin(hdg) * Math.sin(distance/earthRadius) * Math.cos(lat1), Math.cos(distance/earthRadius) - Math.sin(lat1) * Math.sin(lat2)); return L.latLng(lat2 / rad, lng2 / rad); } L.DirectedCircle = L.Circle.extend({ options: { heading: 0, arrowLength: null, arrowWidth: null }, initialize: function(latLng, options) { L.Circle.prototype.initialize.call(this, latLng, options); this._arrow = null; }, onAdd: function(map) { L.Circle.prototype.onAdd.call(this, map); this._updateArrow(); map.addLayer(this._arrow); }, onRemove: function(map) { if (this._arrow) { map.removeLayer(this._arrow); this._arrow = null; } L.Circle.prototype.onRemove.call(this, map); }, _updateArrow: function() { const map = this._map; if (!map) return; const center = this.getLatLng(); const radius = this.getRadius(); const heading = this.options.heading; const arrowLength = this.options.arrowLength || radius * 0.5; const arrowWidth = this.options.arrowWidth || radius * 0.3; const edgePoint = computeDestinationPoint(center, radius, heading); const tipPoint = computeDestinationPoint(center, radius + arrowLength, heading); const leftWing = computeDestinationPoint(tipPoint, arrowWidth, heading - 135); const rightWing = computeDestinationPoint(tipPoint, arrowWidth, heading + 135); const arrowLatLngs = [edgePoint, leftWing, tipPoint, rightWing]; if (this._arrow) { this._arrow.setLatLngs(arrowLatLngs); } else { this._arrow = L.polyline(arrowLatLngs, { color: this.options.color, weight: this.options.weight, fill: true, fillColor: this.options.color, fillOpacity: this.options.fillOpacity }); } }, setHeading: function(heading) { this.options.heading = heading; this._updateArrow(); return this; }, setRadius: function(radius) { L.Circle.prototype.setRadius.call(this, radius); this._updateArrow(); return this; } }); L.directedCircle = function(latLng, options) { return new L.DirectedCircle(latLng, options); }; """ mapWidget.runJavaScript(custom_js) // 创建带方向的圆形标记 mapWidget.runJavaScript(""" L.directedCircle([51.505, -0.09], { radius: 100, heading: 45, color: 'darkblue', fillColor: 'blue', fillOpacity: 0.6 }).addTo(map); """) widget.show() sys.exit(app.exec_())
额外说明
- 如果你的heading定义是0为正东(数学坐标系),只需要在
computeDestinationPoint函数中把hdg = heading * rad改成hdg = (heading - 90) * rad即可。 - 箭头的样式可以完全自定义,比如修改
L.polyline的color、weight、fillOpacity等参数,甚至可以单独设置箭头样式,不跟圆形同步。 - 这个方案完全基于Leaflet原生API,没有依赖第三方插件,性能高效,符合你说的四点折线简单实现的需求。
内容的提问来源于stack exchange,提问作者ZioByte
相关产品推荐
相关产品推荐

