技术应用mapbox-gl
API、mapbox-dem
高程数据、D3
动态计算生成
关键技术
mapbox-dem
预加载D3
动态计算SVG
在无人机航线规划领域,一个常见但棘手的问题是如何确保无人机能够安全地进行仿地飞行(Terrain Following Flight)。今天,我要分享一个结合Mapbox地形数据和D3.js可视化的解决方案,它不仅能帮助我们直观地规划航线,还能实时展示地形起伏与飞行路径的关系。
首先,我们需要从Mapbox获取数字高程模型(DEM)数据。Mapbox提供了queryTerrainElevation
API,让我们能够获取任意坐标点的海拔高度:
typescriptconst { altitudeCalculation } = usePointAltitudeCalculation();
const waypoints = await altitudeCalculation(taskData, map);
为了确保图表能够响应式适应不同屏幕尺寸,我们使用了ResizeObserver
:
typescriptuseEffect(() => {
if (!containerRef.current) return;
const resizeObserver = new ResizeObserver(entries => {
const entry = entries[0];
if (entry) {
const width = entry.contentRect.width;
const height = width * aspectRatio;
setDimensions({ width, height });
}
});
resizeObserver.observe(containerRef.current);
return () => resizeObserver.disconnect();
}, [aspectRatio]);
drawLineChart
函数是整个可视化系统的核心,让我们深入解析其实现细节:
typescript// 清除现有内容,确保图表重绘时不会产生叠加效果
d3.select(svgRef.current).selectAll('*').remove();
// 设置图表边距,预留轴线和标签空间
const margin = { top: 20, right: 30, bottom: 30, left: 40 };
const innerWidth = dimensions.width - margin.left - margin.right;
const innerHeight = dimensions.height - margin.top - margin.bottom;
typescript// 数据分类:区分地形数据和航点数据
const contrastData: WaypointsFeaturesMeta[] = _data;
const dronesData: WaypointsFeaturesMeta[] = _data.filter(d => d.id.indexOf("waypoint-") > -1);
// X轴比例尺:将距离映射到画布宽度
const xScale = d3
.scaleLinear()
.domain([0, d3.max(contrastData, (d: WaypointsFeaturesMeta) => d.distance) || 0])
.range([0, innerWidth]);
// Y轴比例尺:将高度映射到画布高度
const yScale = d3
.scaleLinear()
.domain([
// 最小值:最低海拔减去10米余量
(_data.reduce((min, obj) => obj.altitude < min ? obj.altitude : min, _data[0]?.altitude)) - 10,
// 最大值:最高海拔加上高度再加10米余量
(_data.reduce((max, obj) => (obj.altitude + obj.height) > max ? (obj.altitude + obj.height) : max, _data[0]?.altitude + _data[0]?.height)) + 10
])
.range([innerHeight, 0]);
typescript// 创建主SVG容器并设置位置
const svg = d3
.select(svgRef.current)
.attr('width', dimensions.width)
.attr('height', dimensions.height)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// 定义地形区域的渐变效果
const contrastGradient = svg
.append('defs')
.append('linearGradient')
.attr('id', 'contrastGradient')
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '0%')
.attr('y2', '100%');
// 设置渐变的起始和结束颜色
contrastGradient
.append('stop')
.attr('offset', '0%')
.attr('stop-color', 'rgba(247, 187, 98, 0.3)') // 顶部颜色:淡黄色
.attr('stop-opacity', 1);
contrastGradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'rgba(247, 178, 98, 0)') // 底部颜色:透明
.attr('stop-opacity', 0.1);
typescript// X轴:距离轴
svg.append('g')
.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale))
.attr('color', 'rgba(255, 255, 255, 0.45)') // 设置轴线和刻度颜色
.call(g => g.select('.domain').attr('stroke', 'rgba(255, 255, 255, 0.45)')) // 主轴线颜色
.call(g => g.selectAll('.tick line').attr('stroke', 'rgba(255, 255, 255, 0.45)')) // 刻度线颜色
.append('text')
.attr('x', innerWidth - 20)
.attr('y', 25)
.attr('fill', 'rgba(255, 255, 255, 0.65)')
.text('航点相对距离(m)'); // X轴标签
// Y轴:高度轴
svg.append('g')
.call(d3.axisLeft(yScale))
.attr('color', 'rgba(255, 255, 255, 0.45)')
.call(g => g.select('.domain').attr('stroke', 'rgba(255, 255, 255, 0.45)'))
.call(g => g.selectAll('.tick line').attr('stroke', 'rgba(255, 255, 255, 0.45)'))
.append('text')
.attr('y', -5)
.attr('x', 50)
.attr('fill', 'rgba(255, 255, 255, 0.65)')
.text('地形海拔高度(m)'); // Y轴标签
typescript// 地形区域生成器:创建填充区域
const areaGenerator = d3.area<DataPoint>()
.x((d: WaypointsFeaturesMeta) => xScale(d.distance)) // X坐标映射
.y0(innerHeight) // 底部基线
.y1((d: WaypointsFeaturesMeta) => yScale(d.altitude)); // 顶部轮廓线
// 地形轮廓线生成器
const lineGenerator = d3.line<DataPoint>()
.x((d: WaypointsFeaturesMeta) => xScale(d.distance))
.y((d: WaypointsFeaturesMeta) => yScale(d.altitude));
// 飞行路径线生成器:考虑飞行高度
const _lineGenerator = d3.line<DataPoint>()
.x((d: WaypointsFeaturesMeta) => xScale(d.distance))
.y((d: WaypointsFeaturesMeta) => yScale(d.altitude + d.height));
typescript// 绘制地形填充区域
svg.append('path')
.datum(contrastData)
.attr('class', 'area')
.attr('d', areaGenerator)
.attr('fill', 'url(#contrastGradient)'); // 使用之前定义的渐变
// 绘制地形轮廓线
svg.append('path')
.datum(contrastData)
.attr('class', 'line')
.attr('d', lineGenerator)
.attr('fill', 'none')
.attr('stroke', 'rgba(247, 187, 98, 0.65)') // 黄色轮廓线
.attr('stroke-width', 2);
// 绘制飞行路径线
svg.append('path')
.datum(dronesData)
.attr('d', _lineGenerator)
.attr('fill', 'none')
.attr('stroke', '#01BE10') // 绿色飞行路径
.attr('stroke-width', 2);
typescript// 为每个航点创建标记
dronesData.forEach((d: WaypointsFeaturesMeta, i: number) => {
// 创建航点组
const group = svg.append('g')
.attr('transform', `translate(${xScale(d.distance)},${yScale((d.altitude + d.height))})`)
.attr("id", d.id)
.style('cursor', 'pointer')
.on('click', () => setClickId(d.id)); // 点击事件处理
// 绘制外圆:状态指示
group.append('circle')
.attr('r', 10)
.attr('fill', d.isCorrect ? '#01BE10' : "#FA4444") // 根据状态设置颜色
.attr('stroke', d.isCorrect ? '#01BE10' : "#FA4444")
.attr('stroke-width', 2);
// 绘制内圆:选中状态
group.append('circle')
.attr('r', 9)
.attr('fill', clickId == d.id ? d.isCorrect ? '#10881A' : "#FA4444" : '#fff');
// 添加序号文本
group.append('text')
.attr('dy', 4)
.attr('fill', clickId == d.id ? '#fff' : d.isCorrect ? '#10881A' : "#FA4444")
.attr('text-anchor', 'middle')
.style('font-size', '12px')
.text(i === 0 ? "T" : i + 1); // 首个点显示"T",其他显示序号
});
这个可视化系统在无人机航线规划中具有几个关键价值:
这个基于Mapbox和D3.js的无人机航线可视化方案,成功地将复杂的地形数据转化为直观的视觉呈现。它不仅解决了仿地飞行规划的技术难题,还提供了流畅的用户体验。
未来的优化方向包括:
通过这个项目,我们看到了现代Web技术在专业领域应用的无限可能。期待看到更多基于这个框架的创新应用!
Claude AI
, 我懒得写了, 以上代码给出的是如何绘制出与地图操作对应的图表1、mapbox-gl
初始化时,需要设置styles, 可使用自定义styles也可使用别的
2、初始化之后再load
中进行高程瓦片加载
关键代码
react// map.current 为new mapboxgl.Map()初始化 map.current.addSource('mapbox-dem', { type: 'raster-dem', url: 'mapbox://mapbox.mapbox-terrain-dem-v1', tileSize: 512, maxzoom: 20 }); map.current.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1 });
问题
在官方案例中在获取高程数据时需要进行以下代码
await map.once('idle');
但是我在使用过程中发现此段代码会导致获取高程数据卡顿卡死,且删除也能获取到高程数据
函数altitudeCalculation
为自定义函数,主要是解决绘制的航点根据间隔每2m
来进行高程数据获取并与真正航点进行对比,进而绘制出黄色模块的地形图图表
代码中clickId
是记录地图上或者D3
图上点击了某个航点进行标记,实现图表上以及地图上的颜色标识切换
本文作者:还是夸张一点
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!