地铁已经成为大多数人出行的首选,北京地铁有多条运营线路, 截至 2019 年 12 月,北京市轨道交通路网运营线路达 23 条、总里程 699.3 公里、车站 405 座。2019 年,北京地铁年乘客量达到 45.3 亿人次,日均客流为 1241.1 万人次,单日客运量最高达 1327.46 万人次。由于采用浮动票价,乘客在乘坐地铁时需要知道出发站和目的站所需低票价以避免不必要的浪费,这就需要在乘客购票前够告知此次乘车的票价。因此,每个车站,均要提供从该站出发,到其他所有地铁站的最少票价信息表供乘客购票前查看。如果有新的地铁线路的加入,就会导致价格表的变更,因此需要使用计算机软件自动生成该票价表。
请设计一个地铁票价信息表生成软件,当输入任意起始站后,能够自动计算出以该站为起始点到其他所有各地铁站点(仅限地铁出行)的票价信息表。
1.2 用户需求
1.2.1 文字输入查询
路径查询:用户输入起始站、终点站,点击相应按钮,系统查找输出里程最短的路径。
票价表查询:用户输入起始站,点击相应按钮,系统查找并输出以相应站点为起始站,到地铁线路中每一站的票价、里程等信息。
1.2.2 图形界面查询
显示完整的北京地铁线路图,用户点击选取自己旅程的起点、终点站,点击相应查询按钮,系统在地铁书上突出显示最短路径。
1.3 系统需求
1.3.1 地铁信息数据结构
每个站点的名称、线路等信息;站点之间的连接、距离等关系;最短路查询算法中用到的标记信息等等,都需要被储存在易于查找、更改的数据结构中。
1.3.2 最短路径查询算法
最短路径问题是图论研究中的一个经典算法问题,旨在寻找图(由结点和路径组成的)中两结点之间的最短路径。
在本任务中,输入一个站时,需要能查找到整个图中其他所有站点到他的最短路径及其长度。
1.3.3 TXT 文件读入地铁信息
北京地铁线路基础信息数据通过一个名为“BaseSubWayInfo.txt”的文本文件读入。需要有算法能够读取并切割提取该文本文档中的内容,储存在相应的地铁信息数据结构中。
1.3.4 前端图形界面
需要生成可通按钮、输入框等形式与用户进行交互的 GUI(Graphical User Interface),通过点击按钮、上传文件、输入站点、提交表单等等方式完成用户的查询需求。
1.3.5 前后端交互
前端的图形界面要与后端的数据、算法进行有效、可信的交互。把输入的信息提取出来,作为参数传递给后端相应的算法,获得结果再传回前端图形界面,使得用户的操作能够输出正确的结果。这是本任务中最不好入门,也是我在上面耗费时间最长的部分。
2. 数据结构设计
由于本项目的数据结构由 JavaScript 语言写就。
JavaScript 是 Web 的编程语言。所有现代的 HTML 页面都使用 JavaScript。
2.1 Station 类
2.1.1 需求和思想
Name[]数组储存站名,
2.1.2 代码实现
class Station { constructor (name) { this.name = name; this.lines = []; //this.location = [0]; this.toEdge = undefined; //this.isTransfer = false; //用于求最短路径 this.preStation = undefined; this.isVisited = false; this.distance = Number.MAX_SAFE_INTEGER; this.fare = Number.MAX_SAFE_INTEGER; }
2.2 Edge 类
2.2.1 需求和思想
2.2.2 代码实现
class Edge { constructor(from, to, weight) { this.nextEdge = undefined; //指向邻接表中的下一条边 this.from = from; this.to = to; this.weight = weight; } }
2.3 StationGraph 类
2.3.1 需求和思想
2.3.2 代码实现
class StationsGraph { //无向图 constructor() { //顶点 this.stationArray = []; } StationAdd(stationName, lineNumber) { for (let element of this.stationArray) { if (element.name == stationName) { element.lines.push(lineNumber); element.isTransfer = true; return element; } }; let stationNew = new Station(stationName); stationNew.lines.push(lineNumber); this.stationArray.push(stationNew); return stationNew; }; StationGet(stationName) { for (const iterator of this.stationArray) { if (iterator.name == stationName) return iterator; } return undefined; }; StationGetIndex(stationName) { for (let i = 0; i < this.stationArray.length; i++) { if (this.stationArray[i].name == stationName) return i; } return -1; }; EdgeWeightGet(from, to) { let index = this.StationGetIndex(from); //找到该顶点 if (index != -1) { let stationFrom = this.stationArray[index]; let edge = stationFrom.toEdge; //该顶点无边 if (edge == null) return Number.MAX_SAFE_INTEGER; //有边 else { while ((edge === null || edge === void 0 ? void 0 : edge.nextEdge) != null) { if (edge.from == from && edge.to == to) return edge.weight; else edge = edge === null || edge === void 0 ? void 0 : edge.nextEdge; } } //但是找不到输入的那条 return Number.MAX_SAFE_INTEGER; } //没这个点 else return Number.MAX_SAFE_INTEGER; }; EdgeAdd(from, to, weight) { let index = this.StationGetIndex(from); //找到该顶点 if (index != -1) { let stationFrom = this.stationArray[index]; let edge = stationFrom.toEdge; //该顶点无边 if (edge == undefined) { stationFrom.toEdge = new Edge(from, to, weight); return true; } //该顶点已经有边 else { while ((edge === null || edge === void 0 ? void 0 : edge.nextEdge) != undefined) { edge = edge === null || edge === void 0 ? void 0 : edge.nextEdge; } edge.nextEdge = new Edge(from, to, weight); return true; } } //找不到该顶点 else return false; };
2.4 SubwayData 类
2.4.1 需求和思想
2.4.2 代码实现
class SubwayData { constructor() { this.graph = new StationsGraph(); this.allLines = []; this.lineInfo = []; }; ReadLines(reader) { var subwayInfoArray = reader.result.split("\n"); var lineAmount = subwayInfoArray[0]; //var subwayInfoArray = subwayInfoArray.slice(1).toString(); //切割并读入每一条线 for (let i = 1; i <= lineAmount; i++) { //读入lineInfo let lineInfoArray = subwayInfoArray[i].toString().split(","); this.lineInfo.push([lineInfoArray[0], lineInfoArray[1]], lineInfoArray[2]); //编号 线路名称 线站点数 //读入Stations for (let j = 3; j < lineInfoArray.length; j += 2) { let stationNew = this.graph.StationAdd(lineInfoArray[j], lineInfoArray[1]);//自带检测 if (j > 3) { //若当前添加的站点不是最后一站,建立与上一站的双向边 this.graph.EdgeAdd(lineInfoArray[j - 2], lineInfoArray[j], lineInfoArray[j - 1]); this.graph.EdgeAdd(lineInfoArray[j], lineInfoArray[j - 2], lineInfoArray[j - 1]); debugger; } } } } } /* @function cutLine 用于把地铁各站间的数据分开,提取对应站点、线路 @param {Array} txtArr 读入后被分割的txt数组 注意要保留地铁线路数 {string} spliter 分割符,不填入默认为中文逗号 */ // SubwayData.prototype.cutLine = function (txtArr, spliter) { // var real_spliter = undefined; // if (arguments.length < 2) // real_spliter = ","; // else // real_spliter = spliter; // var linesCounts = txtArr[0]; // for (var i = 1; i <= linesCounts; i++) { // var tempLine = txtArr[i].split(real_spliter); // this.allLines.push(tempLine); /* 添加线路信息 */ // var lineStationCnts = parseInt(tempLine[2]); /* 地铁线路总站数 */ // var lineName = tempLine[1]; /* 地铁线路名称 {string} */ // this.lineInfo.push([lineName, lineStationCnts]); /* 线路信息,站点名称-站点数量 */ // /* 对于每一条线,遍历每个站 */ // for (var j = 3; j < tempLine.length; j += 2) { // // 名字必须要去掉 \r 等回车换行符 // var stationName = tempLine[j].replace(/[\r\n]/g, ""); // var nextStationName = undefined; /* 下一站的名称 */ // if (j !== (tempLine.length - 1)) // nextStationName = tempLine[j + 2].replace(/[\r\n]/g, ""); /* 如果不是最后一站,则建立和下一站的边 */ // if (this.StationController.hasOwnProperty(stationName)) { /* 已经遍历过的站 */ // this.StationController[stationName].onLine.add(lineName); // } // else { /* 如果这是没有遍历过的点 */ // this.StationController[stationName] = new station_1["default"](stationName); // this.StationController[stationName].onLine.add(lineName); // } // } // } // };
3.算法设计
3.1 最短路径算法
Dijkstra(from) { //init this.stationArray.forEach((element) => { if (element.name == from) { element.distance = 0; } else { element.distance = Number.MAX_SAFE_INTEGER; } element.isVisited = false; element.preStation = undefined; }); let curSrc = this.StationGet(from); //理论上可以更新所有, 只要有未被访问的点,就不停下循环 do { if (curSrc.isVisited == true) continue; curSrc.isVisited = true; let edge = curSrc.toEdge; while (edge != undefined) { let curDest = this.StationGet(edge.to); if ((curDest.isVisited) == false && curDest.distance > curSrc.distance + edge.weight) { //更新最短距离 curDest.distance = (curSrc.distance + edge.weight).toFixed(3); //保留三位小数 curDest.distance = Number(curDest.distance); curDest.preStation = curSrc.name; } edge = edge.nextEdge; } //更新cursrc,找到distance最小,且isvisited = false的最近邻接点 var nextSrc = new Station("default"); //default 标志结束循环 nextSrc.distance = Number.MAX_SAFE_INTEGER; for (const iter of this.stationArray) { if (iter.isVisited == false) nextSrc = nextSrc.distance > iter.distance ? iter : nextSrc; } //更新当前遍历的节点 curSrc.fare = curSrc.FareGet(); curSrc = nextSrc; } while (curSrc.name != "default");//default 标志结束循环 } };
3.2 票价计算算法
FareGet() { if (this.distance < 6) return 3; else if (this.distance < 12) return 4; else if (this.distance < 22) return 5; else if (this.distance < 32) return 6; else if (this.distance < 52) return 7; else if (this.distance < 72) return 8; else return 9; } }
3.3 查询算法
Query(from, to) { let pathArray = new Array(); let curStation = this.graph.StationGet(to); this.graph.Dijkstra(from); do { pathArray.push(curStation?.name); curStation = this.graph.StationGet(curStation.preStation); //debugger; } while (curStation.preStation != undefined); pathArray.push(curStation.name); //加入始发站 return pathArray.reverse();
3.4 文件读入算法
ReadLines(reader) { var subwayInfoArray = reader.result.split("\n"); var lineAmount = subwayInfoArray[0]; //var subwayInfoArray = subwayInfoArray.slice(1).toString(); //切割并读入每一条线 for (let i = 1; i <= lineAmount; i++) { //读入lineInfo let lineInfoArray = subwayInfoArray[i].toString().split(","); this.lineInfo.push([lineInfoArray[0], lineInfoArray[1]], lineInfoArray[2]); //编号 线路名称 线站点数 //读入Stations for (let j = 3; j < lineInfoArray.length; j += 2) { let stationNew = this.graph.StationAdd(lineInfoArray[j], lineInfoArray[1]);//自带检测 if (j > 3) { //若当前添加的站点不是最后一站,建立与上一站的双向边 this.graph.EdgeAdd(lineInfoArray[j - 2], lineInfoArray[j], lineInfoArray[j - 1]); this.graph.EdgeAdd(lineInfoArray[j], lineInfoArray[j - 2], lineInfoArray[j - 1]); debugger; } } } }
4.前端界面设计
4.1 所使用的 Web 开发工具
HTML + jQuery + Bootstrap 4 实现主页面和文字输入查询超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页的标准标记语言。可以使用 HTML 来建立 Web 站点,HTML 运行在浏览器上,由浏览器来解析。 jQuery 是一个快速,小型且功能丰富的 JavaScript 库。 通过易于使用的 API(可在多种浏览器中使用),它使 HTML 文档的遍历和操作,事件处理,动画和 AJAX 等事情变得更加简单。 兼具多功能性和可扩展性,jQuery 改变了数百万人编写 JavaScript 的方式。 Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架。Bootstrap 是基于 HTML、CSS、JavaScript 的。Bootstrap 是由 Twitter 的 Mark Otto 和 Jacob Thornton 开发的。Bootstrap 是 2011 年八月在 GitHub 上发布的开源产品
4.2 核心流程
<script type="text/javascript"> class SubwayData { } //初始化总数据结构 let subwayData = new SubwayData(); let pathsData = new Array(); let tableColumns = [ {field: 'id', title: '序号', sortable: true}, {field: 'name', title: '目的地', sortable: true}, {field: 'distance', title: '最短距离', sortable: true}, {field: 'fare', title: '最低票价', sortable: true}, ]; $('#table1').bootstrapTable({//表格初始化 columns: tableColumns, //表头 data: undefined, //通过ajax返回的数据 width:300, height:268, method: 'get', pageSize: 3, pageNumber: 1, pageList: [], cache: false, striped: true, pagination: true, sidePagination: 'client', search: false, showRefresh: false, showExport: false, showFooter: true, // exportTypes: ['csv', 'txt', 'xml'], clickToSelect: false, }); //读取txt var openFile = () => { var input = event.target; var reader = new FileReader(); reader.onload = () => { if (reader.result) { //读入数据txt,数据都存在subwaydata subwayData.ReadLines(reader); //subwayData.Query() } $("#success").show(100); //$("#output").html(reader.result); } reader.readAsText(input.files[0]); }; //query $("#submit").click(() => { var departure = $("#departure").val(); var destination = $("#destination").val(); var pathArray = subwayData.Query(departure, destination); var destStation= subwayData.graph.StationGet(destination); $("#dijpath").html(() => { return pathArray.join(" ==> "); }); $("#path").show(100); $("#dist").html(destStation.distance + "km"); $("#fee").html(destStation.fare + "¥"); $("#fare").show(100); let index = 0; for(const iter of subwayData.graph.stationArray) { if(iter.name == departure) continue; let data = {} data["id"] = index++; data["name"] = iter.name; data["distance"] = iter.distance; data["fare"] = iter.fare; pathsData.push(data); } $('#table1').bootstrapTable("load", pathsData); pathsData = undefined; }); $("#faresheet").click( () => {$('#faretable').show(100);} ); </script>
4.3Amap 辅助实现图形化查询
html> <head> <meta charset="UTF-8"> <!--重要meta, 必须!--> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,shrink-to-fit=no"/> <title>SUBWAY</title> </head> <body> <div id="mysubway"></div> <script src="https://webapi.amap.com/subway?v=1.0&key=5f9f571c1e5291573744a37de7128d46&callback=cbk"></script> <script type="text/javascript"> window.cbk = function(){ var mysubway = subway("mysubway", { easy: 1 }); }; </script> </body> </html>
5.测试
6. 总结&收获
通过本次课设活动,我真正第一次从头到尾整了一个可以用的前端项目。虽然并没有用到什么特别复杂的算法或者框架,但是收获的工程经历、经验是非常宝贵的。完成项目的过程中精力的无数心酸、崩溃、难过并没能压倒我,相反做成了这一次项目之后,我心就有底气了。回过头来看,当时感觉根本不可能解决的那些问题都一步一步迈过去了,根本没有必要惊慌,没有必要自乱阵脚。天塌不下来!好好干活,会好起来的!
完整代码:https://download.csdn.net/download/weixin_55771290/87398299