ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/16 7:25:14 拓冰建站 浏览量
基于百度地图与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 地图、数据、算法、视图四层模块划分源码结构直接决定项目文档好不好写。常见做法是分成四个模块地图模块负责初始化BMapGL.Map添加 Marker 和 Label。数据模块负责把points变成distMatrix包括异步调度、错误重试和缓存。算法模块只接受矩阵不碰 DOM返回访问顺序。视图模块把最终顺序画成 Polyline更新面板上的里程信息。算法模块与地图模块解耦后可以直接在 Node 环境里用假矩阵做单元测试也可以把同一套 TSP 算法复用到打车聚合、快递配送等场景。课程设计答辩时这一条“可测试性”往往是加分项。3. HBuilderX 初始化工程并实现旅行路径规划算法源码现在进入可运行的前端实现。下面的步骤以百度地图 GL 版 JavaScript API 为例开发工具用 HBuilderX。HBuilderX 里创建一个普通 Web 项目配置好 html、css、javascript 后内置浏览器和控制台可以直接调试比手动开本地服务器省事。3.1 最小 HTML 工程和百度地图脚本加载!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title百度地图旅行路径规划/title style body, html { margin: 0; height: 100%; } #map { width: 100%; height: 100%; } /style /head body div idmap/div script typetext/javascript srchttps://api.map.baidu.com/api?typewebglv1.0ak你的密钥/script script srcalgorithm.js/script script srcmain.js/script /body /html这段 html 里最关键的是typewebgl参数它让百度地图加载 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 时可调到 500EPSILON0.001小于该差值视为无改进起点固定0用travelState.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 从输入到路线的五个阶段数据流旅行路径规划算法的核心流程可以概括为五个阶段录入 POI 地点得到一组带经纬度的坐标点。调用百度地图DrivingRoute构建有向距离矩阵。用最近邻算法从起点开始构造初始访问顺序。对初始顺序做 2-opt 局部优化直到没有明显改进。将最终下标数组映射为地图 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中复制pathDistance和twoOpt并把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 分层都能继续复用。本文还有配套的精品资源点击获取