基于坡度的登山路线难度规划与可视化

在上一篇文章里,我实现了一个基于建筑阴影的路线规划算法,能够在夏季推荐更凉爽的路线。这一次,我想把思路延伸到登山场景:当我们规划一条登山路径时,除了距离以外,坡度才是决定难度的关键因素。于是我选取了一个景区内的路网数据(shp 格式)和一份小范围的坡度栅格数据(tif 格式),做了一个 Demo,目标是让用户可以在前端页面选择不同的难度等级,系统自动给出对应的登山路线,同时在地图上渲染出来,并配合图表展示坡度、长度和地形变化情况。

交互效果上,地图部分使用 Mapbox 进行三维可视化,支持卫星底图、倾斜视角和路径渲染;图表部分用 ECharts 绘制折线图、柱状图等,展示沿途坡度变化、不同难度线路的总长度对比、地形起伏分布和阴影覆盖率等指标。这样一来,用户不仅能看到最终推荐的线路,还能直观地了解这条线路“有多陡”“要走多久”,交互体验更加完整。

登山路线规划

技术思路

整体上我还是采用“后端计算 + 前端展示”的架构:

后端(Python):负责数据准备和路线规划,具体包括 shp 矢量数据的加载、坡度栅格的读取、路段坡度计算、加权图构建和最短路径搜索。

前端(Vue + Mapbox + ECharts):负责展示,地图部分渲染路径,图表部分分析坡度和长度指标。

后端核心在于 geopandas、rasterio 和 networkx 这三个库。geopandas 用来处理矢量路网数据,networkx 用来做图论计算。而 rasterio 是栅格处理的关键,它能方便地将地理坐标转换成栅格行列,并读取像元值。因为坡度数据以栅格形式存储,所以 rasterio 在这里承担了桥梁的作用。

原始数据

实现过程

首先是数据加载。矢量路网用 geopandas 读取,坡度栅格用 rasterio 打开。

1
2
3
4
5
6
7
8
import geopandas as gpd
import rasterio
import networkx as nx
import numpy as np

roads = gpd.read_file("scenic_roads.shp") # 路网数据
slope = rasterio.open("slope.tif") # 坡度栅格

在这里,roads 代表景区内的可通行道路,slope 是每个像元对应一个坡度值的栅格数据集。

接着我写了一个函数来计算每条道路的平均坡度。做法是把道路线段采样成一系列点,再通过 rasterio.index(x, y) 将坐标映射到栅格行列,最后取出坡度值并计算平均。

1
2
3
4
5
6
7
8
9
def calc_edge_slope(line, raster):
coords = [(x, y) for x, y in zip(*line.xy)]
slopes = []
for x, y in coords:
row, col = raster.index(x, y)
slopes.append(raster.read(1)[row, col])
return np.nanmean(slopes)


line 是一条道路几何对象,raster.index 方法能把地理坐标对应到栅格行列,然后 raster.read(1) 返回坡度矩阵的第一波段数据。我循环采样点,得到一组坡度值,用 np.nanmean 求均值,避免缺失值影响结果。这样就能给每一条道路分配一个坡度属性。

然后我用 networkx 构建了加权图,把每一条路段作为一条边加入图结构。

1
2
3
4
5
6
7
G = nx.Graph()
for idx, row in roads.iterrows():
length = row.geometry.length
slope_val = calc_edge_slope(row.geometry, slope)
weight = length * (1 + slope_val / 30.0)
G.add_edge(row.start, row.end, weight=weight, slope=slope_val, length=length)

在这里我定义了一个简单的权重函数:道路长度乘以一个坡度因子 (1 + slope/30),这样坡度越大,权重越高,也就是“更难走”。同时我在图里记录了 slope 和 length 作为属性,后续前端可以直接用来展示。

最后,用 networkx.shortest_path 做最优路径搜索,指定起点和终点后,就能得到一条节点序列,表示推荐的路径。

1
2
3
shortest_path = nx.shortest_path(
G, source=start_node, target=end_node, weight="weight"
)

前端展示

前端用 Mapbox 初始化地图,设置中心点、缩放等级和俯视角度,叠加卫星影像作为底图,再将后端返回的 GeoJSON 路径添加到地图上。不同难度的路线用不同颜色区分,比如绿色表示简单,橙色表示中等,红色表示困难。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
map.addSource("route", {
type: "geojson",
data: routeGeoJSON
});

map.addLayer({
id: "route-line",
type: "line",
source: "route",
paint: {
"line-color": "#ff6600",
"line-width": 4
}
});

登山路线-简单
登山路线-中等
登山路线-困难

同时在 Vue 里嵌入 ECharts 图表,例如绘制坡度变化曲线:横轴是距离分段,纵轴是坡度值,能直观看到“这条路爬升有多陡”。

1
2
3
4
5
6
option = {
xAxis: { type: "category", data: distanceBins },
yAxis: { type: "value" },
series: [{ data: slopeValues, type: "line", smooth: true }]
};

除了坡度曲线,我还绘制了不同难度的总长度对比柱状图、坡度分布直方图,以及根据山体阴影计算的覆盖率图表。
登山路线-困难

总结

这次的路线规划 Demo 和上一篇阴影路线规划在方法上是一脉相承的,都是在传统最短路径的基础上引入了新的地理因子。上次我考虑的是建筑阴影,这次则考虑了坡度。不同的是,坡度数据以栅格形式存在,需要通过 rasterio 来与矢量路网结合。

最终实现的效果是,用户可以选择不同的难度等级,系统计算出对应的登山路线,并在地图和图表上给出完整的分析可视化结果。这样能应用于登山场景,徒步等户外导航中。