news 2026/9/16 7:25:08

基于百度地图与JavaScript的多点旅行路径规划算法设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于百度地图与JavaScript的多点旅行路径规划算法设计

简介:面向毕业设计、课程设计与项目开发场景,这份基于JS的百度地图旅行路径规划算法资源,提供了完整可运行的源码、项目文档、算法流程解析与功能介绍。算法以最小生成树为基础,先用克鲁斯卡尔构造带权无向图的最小生成树,再通过贪心策略删除节点度大于二的边,将局部最优逐步转化为全局最优,并利用匹配算法处理一度点,从而解决每个景点只访问一次且总路径最短的旅行规划问题,也适用于理解经典旅行商问题的变体。压缩包共三十四个文件,包含十一份超文本页面、七份脚本、九份可扩展标记配置,以及样式表、标记文档和项目配置,整体仅有八十八KB,目录结构清晰,便于按模块阅读与二次开发。目前已有一百四十人浏览学习,源码经过严格测试,可直接作为毕业设计、课程设计的参考实现,或在此基础上扩展新的路径规划功能。

1. 多点排序才是“百度地图旅行路径规划”真正要解决的问题

普通导航只解决“从 A 到 B”,但毕业设计里最常见的场景是“一天跑 6 个客户、逛 4 个景点、送 8 件货”。人脑按直觉排序时很容易被“当前最近”误导,实际路网距离、红绿灯和单行道会让最终路径多出 20% 甚至更多的里程。基于 javaScript 实现的百度地图旅行路径规划算法,就是把“给定起点和多个目的地,找到一条尽量短的访问顺序”这件事拆成地图展示、距离矩阵计算、最近邻构造和 2-opt 优化四个可独立测试的模块。整套源码和项目文档同时面向毕业设计、课程设计和二次开发,既能直接演示算法效果,也可以换成自己的数据集跑实验。

2. 旅行路径规划问题建模与百度地图 API 选型

写代码之前先把问题变成数学对象。给定 n 个地点,任意两地之间都有“驾车距离”和“驾车耗时”,这就构成一张带权完全图。现实路网里有单行道、高架入口限制,所以 A→B 的距离未必等于 B→A,建议一开始就按有向图处理,避免后期返工。

const travelState = { points: [], // 地点数组,元素是 { name, lng, lat } distMatrix: [], // n x n 驾车距离矩阵,distMatrix[i][j] 表示 i 到 j timeMatrix: [], // n x n 驾车耗时矩阵,单位秒 bestOrder: [] // 最终访问顺序,存储的是 points 的下标 };

这个对象就是整个旅行路径规划算法的全局状态。把地图覆盖物、算法计算和矩阵缓存都收敛到它上面,可以避免页面里堆满全局变量。调试时只需在控制台打印travelState就能看清每一步的输入输出。

2.1 从“旅行安排”到“TSP 完全图”的抽象过程

旅行路径规划算法本质上是 TSP(旅行商问题)的一个变体:从一个固定起点出发,访问所有地点后不需要返回起点,求最短路径。因为不需要回到起点,所以它比标准 TSP 稍微简单,但仍属于 NP-Hard 问题,没有办法保证多项式时间内找到全局最优解。

我一般会在文档里这样写输入输出:

  • 输入:travelState.points,至少包含经纬度和名称。
  • 输出:travelState.bestOrder,例如[0, 3, 1, 2]表示从第 0 号点出发,先后访问 3、1、2 号点。
  • 权重:优先使用驾车距离,因为用户对“公里数”更敏感;如果要做时间窗约束,再用timeMatrix作为第二权重。

边权重不对称这一点容易被忽略。如果直接按对称矩阵优化,算法结果可能在实际路网上变成“无法左转”的假路径。所以我在代码里保留isDirected的判断,构造矩阵时默认把distMatrix[i][j]distMatrix[j][i]分开请求。

2.2 百度地图 JavaScript API 与 Web 服务距离矩阵的选型对比

百度地图相关能力并不只有一种取法。做毕业设计前先列一张对比表,可以省下大量重写时间。

方案数据来源请求方式优点局限
BMapGL.DrivingRoute浏览器端 JavaScript API每次计算一对点直接在地图上看到轨迹,无需后端循环请求多,耗时久,配额消耗快
Web 服务距离矩阵百度地图 Web 服务 API后端或代理请求一次请求可算多对点,适合矩阵构建浏览器直接 fetch 有跨域限制,需要代理
静态图 + 手工数据无实时请求算法调试最稳定无法体现“真实道路距离”

对于纯 javaScript 前端项目,我最常用BMapGL.DrivingRoute循环求距离。它返回的结果里直接包含距离和耗时,还能顺便拿到路线几何数据,适合在地图上展示。虽然请求量为 n×(n−1) 次,但 10 个点以内的毕业设计场景完全撑得住。

如果导师允许加一个 Node 代理,我会优先换 Web 服务距离矩阵。前端只发一个经纬度数组,后端拼参数、做缓存、控制并发,前端代码会简单很多。这套设计写进项目文档里,也能体现你对前后端职责划分的思考。

2.3 地图、数据、算法、视图四层模块划分

源码结构直接决定项目文档好不好写。常见做法是分成四个模块:

  1. 地图模块:负责初始化BMapGL.Map,添加 Marker 和 Label。
  2. 数据模块:负责把points变成distMatrix,包括异步调度、错误重试和缓存。
  3. 算法模块:只接受矩阵,不碰 DOM,返回访问顺序。
  4. 视图模块:把最终顺序画成 Polyline,更新面板上的里程信息。

算法模块与地图模块解耦后,可以直接在 Node 环境里用假矩阵做单元测试,也可以把同一套 TSP 算法复用到打车聚合、快递配送等场景。课程设计答辩时,这一条“可测试性”往往是加分项。

3. HBuilderX 初始化工程并实现旅行路径规划算法源码

现在进入可运行的前端实现。下面的步骤以百度地图 GL 版 JavaScript API 为例,开发工具用 HBuilderX。HBuilderX 里创建一个普通 Web 项目,配置好 html、css、javascript 后,内置浏览器和控制台可以直接调试,比手动开本地服务器省事。

3.1 最小 HTML 工程和百度地图脚本加载

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>百度地图旅行路径规划</title> <style> body, html { margin: 0; height: 100%; } #map { width: 100%; height: 100%; } </style> </head> <body> <div id="map"></div> <script type="text/javascript" src="https://api.map.baidu.com/api?type=webgl&v=1.0&ak=你的密钥"></script> <script src="algorithm.js"></script> <script src="main.js"></script> </body> </html>

这段 html 里最关键的是type=webgl参数,它让百度地图加载 GL 版命名空间BMapGL。如果换用旧版 v3.0,命名空间就变成BMap,下面代码里的BMapGL.Map要同步替换。ak参数必须在百度地图控制台申请,并且要配置域名白名单,否则本地打开页面会报“校验失败”。

3.2 初始化地图和添加 POI 地点的 JavaScript 函数

const map = new BMapGL.Map('map'); map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12); map.enableScrollWheelZoom(true); function addPoint(name, lng, lat) { const point = new BMapGL.Point(lng, lat); const marker = new BMapGL.Marker(point); const label = new BMapGL.Label(name, { position: point, offset: new BMapGL.Size(-12, -28) }); map.addOverlay(marker); map.addOverlay(label); travelState.points.push({ name, lng, lat }); }

centerAndZoom的第一个参数是地图中心点,第二个是缩放级别。城市级用例设置 12,如果是整个区县可以放宽到 10。Label的 offset 需要根据文字宽度微调,避免压住 Marker。这里所有地点都直接使用百度坐标 BD-09,不要混用其他坐标系,否则后面绘制路线时会出现几十米的偏移。

3.3 用 Promise 封装驾车距离获取并构建距离矩阵

BMapGL.DrivingRoute的回调风格和现代 JavaScript 不太合拍。我习惯用 JavaScript 函数把它包成 Promise,再用async/await串行构建矩阵。串行虽然慢一点,但不容易触发百度地图配额限制。

function getDrivingSegment(fromIdx, toIdx) { return new Promise((resolve, reject) => { const start = travelState.points[fromIdx]; const end = travelState.points[toIdx]; const driving = new BMapGL.DrivingRoute(map, { onSearchComplete(results) { if (driving.getStatus() === BMAP_STATUS_SUCCESS) { const route = results.getPlan(0).getRoute(0); resolve({ distance: route.getDistance(), duration: route.getDuration() }); } else { reject(new Error(`路线规划失败,状态码 ${driving.getStatus()}`)); } } }); driving.search(new BMapGL.Point(start.lng, start.lat), new BMapGL.Point(end.lng, end.lat)); }); } async function buildDistanceMatrix() { const n = travelState.points.length; for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { if (i === j) continue; try { const res = await getDrivingSegment(i, j); travelState.distMatrix[i][j] = res.distance; travelState.timeMatrix[i][j] = res.duration; } catch (e) { console.error(`计算 ${i} -> ${j} 失败`, e); } } } }

这里需要注意getRoute(0)读取的是第一条子路线。某些跨城路线会返回多条 plan,默认取第一条即可;如果追求更严格的结果,可以对所有 plan 做getDistance()求最小值。代码里的travelState.distMatrix[i][j]是方向性的,i 到 j 和 j 到 i 各算一次,因此矩阵构建的时间复杂度是 O(n²) 次网络请求,而不是 O(n)。

3.4 最近邻 + 2-opt 优化算法源码

矩阵建好后,算法部分就是纯 javaScript 计算。先用最近邻构造初始解,再用 2-opt 局部优化。最近邻的时间复杂度是 O(n²),2-opt 在 50 个点以内表现稳定。

const MAX_ITER = 100; const EPSILON = 0.001; function pathDistance(order) { let total = 0; for (let i = 0; i < order.length - 1; i++) { const dist = travelState.distMatrix[order[i]][order[i + 1]]; total += dist || 0; } return total; } function nearestNeighbor() { const n = travelState.points.length; const visited = new Array(n).fill(false); const order = [0]; visited[0] = true; for (let k = 1; k < n; k++) { const current = order[order.length - 1]; let nearest = -1; let nearestDist = Infinity; for (let i = 0; i < n; i++) { const dist = travelState.distMatrix[current][i]; if (!visited[i] && dist != null && dist < nearestDist) { nearestDist = dist; nearest = i; } } order.push(nearest); visited[nearest] = true; } return order; } function twoOpt(order) { let best = order.slice(); let improved = true; let iter = 0; while (improved && iter < MAX_ITER) { improved = false; iter++; for (let i = 1; i < best.length - 1; i++) { for (let j = i + 1; j < best.length; j++) { const candidate = best .slice(0, i) .concat(best.slice(i, j + 1).reverse()) .concat(best.slice(j + 1)); if (pathDistance(candidate) < pathDistance(best) - EPSILON) { best = candidate; improved = true; } } } } return best; } const nnOrder = nearestNeighbor(); const finalOrder = twoOpt(nnOrder); console.log('最近邻里程', pathDistance(nnOrder)); console.log('优化后里程', pathDistance(finalOrder));

这段算法源码有几个关键点。i从 1 开始,是为了固定起点下标 0 不被反转队列带走。pathDistance只计算访问顺序相邻节点间的距离,不闭合回路,因为旅行路径规划通常不需要回到起点。如果需求变成“送完货还要回仓库”,只需要在路径末尾补一个起点下标再参与计算即可。EPSILON用来过滤浮点数抖动导致的无效优化。

这里的参数可以按数据规模微调:

参数建议值说明
MAX_ITER100节点数超过 30 时可调到 500
EPSILON0.001小于该差值视为无改进
起点固定0travelState.points[0]作为出发点
矩阵方向有向保留 A→B 和 B→A 两个距离

3.5 把优化后的访问顺序绘制成百度地图路线

算法返回的finalOrder是一组下标,比如[0, 3, 1, 2]。要把它变成可视化结果,需要把这些下标映射回坐标点,再添加 Polyline 覆盖物。

function drawOptimizedRoute(order) { map.clearOverlays(); const routePoints = order.map(idx => { const p = travelState.points[idx]; return new BMapGL.Point(p.lng, p.lat); }); const polyline = new BMapGL.Polyline(routePoints, { strokeColor: '#1677ff', strokeWeight: 6, strokeOpacity: 0.8 }); map.addOverlay(polyline); travelState.points.forEach((p, idx) => { const marker = new BMapGL.Marker(new BMapGL.Point(p.lng, p.lat)); map.addOverlay(marker); }); map.setViewport(routePoints); }

clearOverlays()会一次性清掉地图上的全部覆盖物,所以要把 Marker 重新添加回来。setViewport的作用是自动调整视野,让所有路线点完整出现在地图可视区内。对于课程设计演示来说,这一步做完就已经具备完整的“输入地点 → 自动排序 → 绘制路线”链路。

4. 算法流程解析与项目文档落盘技巧

毕业设计答辩不会只看能跑的界面,更看重项目文档里能不能把算法流程讲清楚。这一章把“从坐标到路线”的完整流程拆开,并给出可以直接写进文档的结构化内容。

4.1 从输入到路线的五个阶段数据流

旅行路径规划算法的核心流程可以概括为五个阶段:

  1. 录入 POI 地点,得到一组带经纬度的坐标点。
  2. 调用百度地图DrivingRoute构建有向距离矩阵。
  3. 用最近邻算法从起点开始构造初始访问顺序。
  4. 对初始顺序做 2-opt 局部优化,直到没有明显改进。
  5. 将最终下标数组映射为地图 Polyline 并展示。

如果要在论文或课程设计文档里画流程图,直接用以下伪代码代替手画图:

输入:points, distMatrix 输出:order order = nearestNeighbor(points) repeat: improved = false for i = 1 to order.length - 2: for j = i + 1 to order.length - 1: candidate = reverseSegment(order, i, j) if distance(candidate) < distance(order) - EPSILON: order = candidate improved = true until improved == false or iteration >= MAX_ITER

这个流程解析的重点是“起点固定”和“路径不闭合”。很多学生作业直接套用标准 TSP 的 2-opt,默认回路闭合,结果在旅行场景里总会多绕一段回头路。我在文档里会特别标注这一区别,因为答辩时老师经常追问。

4.2 项目文档里的算法对比表和模块设计

写项目文档时,不要只贴代码,要放一张实验结果对比表。我可以给一个模板,用你实际跑出的数据替换“示例值”即可:

场景节点数最近邻总里程最近邻 + 2-opt 总里程提升比例
示例数据842.5 km36.8 km13.4%
示例数据15109.7 km88.2 km19.6%

这类表格同时出现在“算法流程解析”和“测试分析”两章中,会显得数据链完整。 除了结果表,还建议给每个核心 JavaScript 函数写一段模块说明:

  • buildDistanceMatrix:负责网络请求,返回 Promise,失败时打印日志但不中断。
  • nearestNeighbor:纯函数,输入矩阵,输出初始顺序。
  • twoOpt:纯函数,输入初始顺序,输出优化后的顺序。
  • drawOptimizedRoute:只做视图渲染,不参与算法计算。

把函数职责写清楚后,答辩老师问“如果地点增加 10 倍怎么办”,你就能顺势引出复杂度分析。最近邻是 O(n²),2-opt 每轮最坏执行 O(n³) 的距离计算。这也是为什么下一章要把算法放进 Web Worker 处理的原因。

4.3 常见运行时报错、状态码与百度地图 API 调试

百度地图相关的 JavaScript 运行时报错,大多数不是算法问题,而是 API 使用姿势问题。

报错现象可能原因处理方式
BMapGL is not definedAPI 脚本未加载,密钥错误或网络不通在控制台 Network 面板查看 api 请求
NETWORK_ERROR域名白名单没有配置百度地图控制台里添加运行域名
请求频繁被拒绝并发请求太多触发配额限制串行构建矩阵或加请求队列
路线坐标发生偏移传入的是 GCJ-02 或 WGS-84 坐标统一转换为百度 BD-09 坐标

我在buildDistanceMatrix中会把错误打印到控制台而不是抛出后中断,这样即使个别路段失败,其他点的矩阵数据仍然可用。如果某个distMatrix[i][j]缺失,路径规划算法会跳过该边,最终结果可能退化为不可达顺序,所以文档中也建议加入矩阵完整性校验,例如判断travelState.distMatrix.length === n * n

5. 20 个点之后的旅行路径规划算法验证与性能提升

当地点数量达到 20 以上,最近邻加上 2-opt 依然能跑,但页面可能会因为频繁计算距离而卡顿。这一章给出最实用的两个优化手段。

5.1 固定起点矩阵验证算法结果是否稳定

旅行路径规划算法是确定性的,因为最近邻和 2-opt 都不包含随机数。我第一次跑完总会用下面的代码确认优化是否真实有效:

const nnOrder = nearestNeighbor(); const optOrder = twoOpt(nnOrder); const before = pathDistance(nnOrder); const after = pathDistance(optOrder); console.log(`优化提升 ${((before - after) / before * 100).toFixed(1)}%`);

多跑几组随机坐标后,如果提升比例忽高忽低,属于正常现象,因为最近邻的初始解质量依赖点的分布。如果出现负提升,说明twoOpt里的反转逻辑改变了起点位置,检查 i 是否从 1 开始即可。

5.2 把 2-opt 计算放进 Web Worker,避免 UI 卡死

主线程里执行两层循环会影响地图拖动。常见做法是新建tsp-worker.js,把算法从渲染层剥离。

const worker = new Worker('tsp-worker.js'); worker.postMessage({ distMatrix: travelState.distMatrix, currentOrder: nnOrder }); worker.onmessage = (e) => { drawOptimizedRoute(e.data.order); };

tsp-worker.js中复制pathDistancetwoOpt,并把onmessage作为入口:

self.onmessage = (e) => { const { distMatrix, currentOrder } = e.data; const result = twoOptWithMatrix(currentOrder, distMatrix); self.postMessage({ order: result }); };

Worker 没有 DOM 访问权限,所以里面不能调用BMapGL,只能做矩阵运算。我把地图相关代码全部留在主线程,这个分层已经足够覆盖绝大多数课程设计和毕业设计场景。

5.3 把“不返回起点”的闭合判定写成配置项

最后留一个值得雕琢的小细节:在很多外卖聚合、巡检路线项目中,“是否返回起点”是一个动态配置,而不是写死的逻辑。在算法模块里增加一个returnToStart参数,路径距离计算时在前端闭合即可。

function pathDistance(order, returnToStart = false) { let total = 0; for (let i = 0; i < order.length - 1; i++) { total += travelState.distMatrix[order[i]][order[i + 1]]; } if (returnToStart) { total += travelState.distMatrix[order[order.length - 1]][order[0]]; } return total; }

这样一个函数就能兼容“旅行”和“巡店”两种语义。把returnToStart写进项目文档,相当于给算法流程解析增加了一个可扩展点,后续无论接入到达时间窗还是多车辆约束,矩阵缓存和 Worker 分层都能继续复用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 7:23:10

AR+AI双引擎如何上云?眼镜跨端融合的架构与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 7:22:16

STM32C5+LSM6DSV16X:SPI轮询读取陀螺仪数据详解

前阵子把一颗LSM6DSV16X接到了STM32C5的板子上&#xff0c;想快速验证陀螺仪能不能正常出数。折腾一圈下来发现&#xff0c;这套组合跟网上大多数教程用的老平台不太一样&#xff0c;寄存器表更新过&#xff0c;CubeMX配置也有几个容易忽略的细节。这篇文章是这个系列的第一篇&…

作者头像 李华
网站建设 2026/9/16 7:21:53

电能质量仪表开发实战:基于即用型平台的标准符合性设计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 7:21:36

macOS隐形启动器:SwiftUI+AppKit混合开发实战

1. 这个“藏在屏幕边缘”的启动器&#xff0c;到底解决了什么真实痛点&#xff1f;你有没有过这样的时刻&#xff1a;正在写方案&#xff0c;突然需要查一个文档路径&#xff1b;开会时临时要打开备忘录记下关键点&#xff1b;或者刚切到 Safari&#xff0c;又想起得去 Termina…

作者头像 李华
网站建设 2026/9/16 7:20:26

Wireshark数据包长度统计实战:从字段解析到发送方向分析

有次线上排查应用响应慢&#xff0c;抓完包一百多万个&#xff0c;我盯着报文列表翻了五分钟头皮发麻。后来改了个习惯&#xff1a;抓到 pcap 之后&#xff0c;先看长度。Wireshark 抓包里&#xff0c;“数据包长度”是一组最不起眼的数字&#xff0c;却是最快能把流量分类的钥…

作者头像 李华
网站建设 2026/9/16 7:19:35

嵌入式数据采集新思路:事件驱动与无锁环形缓冲的轻量级实践

去年年底做一台老旧控制器的数据接入改造&#xff0c;设备还是armv7架构&#xff0c;内存总共64MB&#xff0c;原来跑着一套采集程序用的是轮询加阻塞队列&#xff0c;CPU常年占用30%以上&#xff0c;温度稍微高点系统就卡成幻灯片。换了好几个开源采集框架&#xff0c;要么交叉…

作者头像 李华