如何在ReactJS的Mapbox地图中绘制可动态调整的半圆?
在React + Mapbox中绘制动态角度半圆的实现方案
核心思路
借助Mapbox的GeoJSON Source和fill图层,通过计算半圆的地理坐标点生成Polygon几何图形,结合React状态控制图层显隐,完全适配动态角度、固定半径的需求,无需依赖图片资源。
实现步骤
1. 生成半圆的GeoJSON坐标
编写函数,根据中心经纬度、半径(米)、起始/结束角度,生成闭合的Polygon坐标集合。可以用Turf.js简化坐标计算,也可自行实现球面坐标转换逻辑。
方案一:使用Turf.js(推荐)
// 先安装依赖:npm install @turf/turf import { destination, point } from '@turf/turf'; // 生成半圆的GeoJSON Feature const createSemicircleGeoJSON = (centerLng, centerLat, radiusMeters, startAngle, endAngle) => { const points = []; // 先添加中心点,用于闭合Polygon points.push([centerLng, centerLat]); // 角度步长,值越小半圆边缘越平滑 const step = 1; let currentAngle = startAngle; // 循环计算半圆上的每个点 while (Math.abs(currentAngle - endAngle) > step) { // Turf.destination:从中心点沿指定角度(正北为0度,顺时针递增)移动指定距离 const destPoint = destination(point([centerLng, centerLat]), radiusMeters / 1000, currentAngle, { units: 'kilometers' }); points.push(destPoint.geometry.coordinates); currentAngle = (currentAngle + step) % 360; } // 添加结束点和中心点,完成闭合 const endPoint = destination(point([centerLng, centerLat]), radiusMeters / 1000, endAngle, { units: 'kilometers' }); points.push(endPoint.geometry.coordinates); points.push([centerLng, centerLat]); return { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [points] } }; };
方案二:无依赖实现坐标计算
如果不想引入Turf,可自行实现球面坐标转换:
const degreesToRadians = (deg) => deg * Math.PI / 180; const radiansToDegrees = (rad) => rad * 180 / Math.PI; // 计算指定角度和距离的目标坐标 const getDestinationPoint = (lat, lng, distanceMeters, angleDeg) => { const earthRadius = 6378137; // 地球半径(米) const latRad = degreesToRadians(lat); const lngRad = degreesToRadians(lng); const angleRad = degreesToRadians(angleDeg); const newLatRad = Math.asin( Math.sin(latRad) * Math.cos(distanceMeters / earthRadius) + Math.cos(latRad) * Math.sin(distanceMeters / earthRadius) * Math.cos(angleRad) ); const newLngRad = lngRad + Math.atan2( Math.sin(angleRad) * Math.sin(distanceMeters / earthRadius) * Math.cos(latRad), Math.cos(distanceMeters / earthRadius) - Math.sin(latRad) * Math.sin(newLatRad) ); return [radiansToDegrees(newLngRad), radiansToDegrees(newLatRad)]; }; // 基于上述函数生成半圆GeoJSON const createSemicircleGeoJSON = (centerLng, centerLat, radiusMeters, startAngle, endAngle) => { const points = [[centerLng, centerLat]]; const step = 1; let currentAngle = startAngle; while (Math.abs(currentAngle - endAngle) > step) { points.push(getDestinationPoint(centerLat, centerLng, radiusMeters, currentAngle)); currentAngle = (currentAngle + step) % 360; } points.push(getDestinationPoint(centerLat, centerLng, radiusMeters, endAngle)); points.push([centerLng, centerLat]); return { type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [points] } }; };
2. React组件中集成Mapbox渲染半圆
结合React状态管理,控制半圆的显示/隐藏、动态更新角度和半径:
import React, { useEffect, useRef, useState } from 'react'; import mapboxgl from 'mapbox-gl'; import { createSemicircleGeoJSON } from './utils'; mapboxgl.accessToken = '你的Mapbox Token'; const MapWithSemicircle = () => { const mapContainerRef = useRef(null); const mapRef = useRef(null); // 设备状态:控制半圆显隐 const [deviceActive, setDeviceActive] = useState(false); // 从服务器获取的固定半径(示例值) const [radius, setRadius] = useState(500); // 动态角度范围(示例值:从正东到正西) const [angleRange, setAngleRange] = useState({ start: 90, end: 270 }); // 半圆中心点坐标(可从服务器获取) const [center, setCenter] = useState([116.3972, 39.9075]); // 初始化地图 useEffect(() => { if (!mapRef.current) { mapRef.current = new mapboxgl.Map({ container: mapContainerRef.current, style: 'mapbox://styles/mapbox/light-v11', // 替换为你的基础样式,需包含浅蓝色底图 center: center, zoom: 15 }); } const map = mapRef.current; map.on('load', () => { // 添加半圆的数据源 map.addSource('semicircle-source', { type: 'geojson', data: createSemicircleGeoJSON(center[0], center[1], radius, angleRange.start, angleRange.end) }); // 添加半圆填充图层,设置深蓝色,叠加在浅蓝色区域上 map.addLayer({ id: 'semicircle-layer', type: 'fill', source: 'semicircle-source', paint: { 'fill-color': '#003366', // 深蓝色 'fill-opacity': 0.7 // 调整透明度确保叠加可见 }, layout: { visibility: deviceActive ? 'visible' : 'none' } }); // 若需控制图层叠加顺序,可添加beforeId参数:beforeId: '你的浅蓝色图层ID' }); // 清理函数 return () => { mapRef.current?.remove(); mapRef.current = null; }; }, []); // 监听状态变化,更新半圆数据和显隐 useEffect(() => { const map = mapRef.current; if (!map || !map.getSource('semicircle-source')) return; // 更新半圆坐标数据 map.getSource('semicircle-source').setData( createSemicircleGeoJSON(center[0], center[1], radius, angleRange.start, angleRange.end) ); // 更新图层可见性 map.setLayoutProperty('semicircle-layer', 'visibility', deviceActive ? 'visible' : 'none'); }, [deviceActive, radius, angleRange, center]); return ( <div> <div ref={mapContainerRef} style={{ width: '100%', height: '600px' }} /> {/* 示例控制按钮,模拟设备开关 */} <button onClick={() => setDeviceActive(!deviceActive)}> {deviceActive ? '关闭设备' : '开启设备'} </button> {/* 示例角度控制,模拟动态角度更新 */} <div> 起始角度:<input type="number" value={angleRange.start} onChange={(e) => setAngleRange(prev => ({ ...prev, start: parseInt(e.target.value) }))} min={0} max={359} /> 结束角度:<input type="number" value={angleRange.end} onChange={(e) => setAngleRange(prev => ({ ...prev, end: parseInt(e.target.value) }))} min={0} max={359} /> </div> </div> ); }; export default MapWithSemicircle;
3. 关键细节说明
- 角度基准:Mapbox和Turf的角度以正北为0度,顺时针递增。若你的业务角度基准不同(比如正东为0),可通过
angle = (inputAngle + 90) % 360转换。 - 性能优化:若角度更新频繁,可增大步长(比如5度)减少坐标点数量,提升渲染效率。
- 图层叠加:若浅蓝色区域是独立图层,可通过
addLayer的beforeId参数,将半圆图层放在它上方。
内容的提问来源于stack exchange,提问作者Eduardo-Maia
相关产品推荐
相关产品推荐

