目录
一、项目介绍及相关要求
设计一个学校导游系统实现最短路径、遍历。
具体实现以下功能:
- 学校地图(graph)输入和修改
- 求两个地点之间的最短路径
- 给定起点,生成遍历学校所有地点的方案
可以使用包括C语言、C++、python、Javascript、Java、go语言、php等各种编程语言,可以使用第三方地图模块,但前提是地图的创建必须自己设计数据结构,可以自己设计邻接矩阵或邻接表,不能够直接调用所使用语言自带的数据结构模块,数据结构要求100%原创。
二、相关模块概括介绍
在这个大作业课设中我选择使用Javascript语言设计邻接表,因为在制作地图时我可以结合web前端的相关知识设计一个可交互地图。使用Python实现相关数据传递,这样我们可以很方便地将数据保存到数据库中,并实时访问数据库提取相关数据。
当然,使用C语言、C++可以结合MFC+GDI设计界面,使用Python可以使用PyQt5或PySide2设计界面等等,看自己的需求。
下面介绍一下我使用的相关模块:
1、fastapi模块
fastapi模块是一个用于前后端数据传递交互的模块,配合ajax可以实现前后端数据传递,当我们学习到前后端交互时都会涉及到这个模块,通过fastapi模块,我们可以通过Python代码将数据保存到数据库中,数据也可以被我们实时调用。
下载fastapi:这个模块可以直接在命令行命令,先切换到对应虚拟环境,输入 pip install fastapi 即可下载。
2、leaflet模块
leaflet模块是一个封装好的地图模块,直接使用里面的一些代码,可以快速地制作一个简略地图。
下载leaflet:Download - Leaflet - a JavaScript library for interactive maps
3、Bootstrap5模块
Bootstrap5模块是用来对前端界面进行美化的,在这个大作业里面使用的不多。
下载Bootstrap5:https://v5.bootcss.com/docs/getting-started/download/
4、jquery模块
jquery模块应该大家使用过,它就是一个Javascript库,里面的代码可以直接使用。
下载jquery:jQuery
三、数据库设计与相关模块较详细介绍
学校导游参观系统,需要我们自己设计出校园地图,这是我第一次接触地图的相关设计与制作,所以我只制作了一个很简陋的地图,地图上只包括点,线以及注释。这个大作业的真实目的是为了让我们学会数据结构中的图论的相关数据结构与算法,学会邻接矩阵与邻接表的设计,学会深度优先遍历、广度优先遍历、迪杰斯特拉算法等等知识,这个才是重点。
1、数据库设计
在本次作业中我使用的数据库是PostgreSQL,这个数据库和MySQL有相似之处,也有不同,由于我刚接触PostgreSQL,所以我就拿来练练手,不过使用MySQL是没有问题的,但是相对来说PostgreSQL要更好一些。
如果想要安装并使用PostgreSQL数据库的,可以看看我的另一篇文章:PostgreSQL数据库安装与启动
下面介绍一下数据库:
数据库的设计有一点复杂,因为所有的数据表不是一开始全部建好了的,是在用户初始化一个新的地图时创建对应的数据表。比如:我初始化了一个名为nete的新地图,那么通过代码会自动创建三张表,分别自动命名为:map_point_nete | map_edge_nete | map_tu_nete |。第一张map_point_nete表保存的是地图上所有点的信息,第一张map_edge_nete表保存的是地图上所有边的信息,第三张map_tu_nete保存的是该地图的相关信息。
就以上面地图的为例:
map_point_nete
map_edge_nete
map_tu_nete
2、leaflet图形库介绍
leaflet算是一个很强大的开源图形库,用来制作地图十分容易上手,我们只需要调用leaflet库中的代码就可以直接形成一个简易的地图。
下载leaflet:Download - Leaflet - a JavaScript library for interactive maps
下载好解压缩leaflet包后,我们只需要leaflet.js和leaflet.css这两个文件就可以了。
如何使用leaflet模块呢?
第一步:新建一个html文件,调用leaflet.css以及leaflet.js。
第二步:新建一个js文件,我们需要在js文件中调用leaflet地图模块,进行地图设计,同时在html文件中显示地图。
在html文件中引入js文件和jquery文件
第三步:在html文件中构造一个div容器,id设置为map,用来存放地图。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地图</title>
<link rel="stylesheet" href="./../leaflet/leaflet.css">
<script src="./../leaflet/leaflet.js"></script>
</head>
<body>
<div id="map" style="height: 670px; width: 1200px; float: left;"></div>
</body>
<script src="./test.js"></script>
<script src="./../jquery_min_3.7.1.js"></script>
</html>
第四步:在js文件中写代码,初始化地图
// 初始化整个地图
var map = L.map('map', {
attributionControl: false,
}).setView([47, 47], 10);
// 上面代码的意思是以(47,47)坐标为中心,放缩等级10创建了一个地图(关于放缩等级,可以自己修改其他数字试一试)
到这一步为止,html文件显示的效果应该是这样的,这是一张空白地图。
一张简略但完整的地图需要有点、线、注释。
接下来我们来添加点:
在js文件中写如下代码
// 初始化整个地图
var map = L.map('map', {
attributionControl: false,
}).setView([47, 47], 10);
var point = L.marker([47.0, 47.0], {draggable: false});
point.addTo(map);
// 第一行定义一个点point,坐标为(47,47)
// 第二行作用是将点添加到地图上
到这一步效果是下图这样的,地图上出现了一个点。
接着添加点的注释,说明该点的地理信息,这里需要修改一下上面的代码,我选择将点和注释绑定在一起之后再添加到地图上。
// 初始化整个地图
var map = L.map('map', {
attributionControl: false,
}).setView([47, 47], 10);
var point = L.marker([47.0, 47.0], {draggable: false});
var customIcon = L.divIcon({
className: 'custom-div-icon',
html: "<div>学校大门</div>",
iconSize: [100, 40], // 调整图标大小
iconAnchor: [,] // 调整图标锚点
});
var marker_with_text = L.marker([47, 47], {
icon: customIcon,
interactive: false // 防止覆盖默认标记的交互
}).addTo(map);
var markerGroup = L.layerGroup([point, marker_with_text]);
markerGroup.addTo(map);
// 将点point和注释marker_with_text绑定到一起组成markerGroup再添加到地图上
到这一步效果是下图这样的,地图上有一个带注释的点。
下一步我们添加线段,先再创建一个点,两点之间连线。
// 初始化整个地图
var map = L.map('map', {
attributionControl: false,
}).setView([47, 47], 10);
var point = L.marker([47.0, 47.0], {draggable: false});
var customIcon = L.divIcon({
className: 'custom-div-icon',
html: "<div>学校大门</div>",
iconSize: [100, 40], // 调整图标大小
iconAnchor: [,] // 调整图标锚点
});
var marker_with_text = L.marker([47, 47], {
icon: customIcon,
interactive: false // 防止覆盖默认标记的交互
}).addTo(map);
var markerGroup = L.layerGroup([point, marker_with_text]);
markerGroup.addTo(map);
var point1 = L.marker([47.1, 47.1], {draggable: false});
var customIcon1 = L.divIcon({
className: 'custom-div-icon',
html: "<div>宿舍</div>",
iconSize: [100, 40], // 调整图标大小
iconAnchor: [,] // 调整图标锚点
});
var marker_with_text1 = L.marker([47.1, 47.1], {
icon: customIcon1,
interactive: false // 防止覆盖默认标记的交互
}).addTo(map);
var markerGroup1 = L.layerGroup([point1, marker_with_text1]);
markerGroup1.addTo(map);
var polyline = L.polyline([[47.0,47.0], [47.1,47.1]], { color: 'blue' });
polyline.addTo(map)
到这一步效果如图,两点之间有线段连接。
到这一步leaflet模块的基础使用就差不多了,我们可以继续不断添加点和线来设计出一个校园地图。
3、fastapi模块介绍
fastapi是一个在Python中使用的模块,可以配合Javascript中的ajax来实现前后端数据的传递与交互。
下面是一个最最基础的代码框架:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get('/')
async def welcome():
return {'message':"学校参观系统"}
if __name__ == '__main__':
import uvicorn
uvicorn.run(
app = "main:app",
host = "127.0.0.1",
port = 12347,
reload = True,
workers = 10
)
其中使用get方法输出“学校参观系统”这几个字。
除此之外,我们还可以使用post方法,put方法以及delete方法配合PostgreSQL数据库可以实现对数据的保存与调用,详细的我在后面解释,涉及到docs文档测试,可以看看fastapi官方文档:FastAPI
四、数据结构设计(重点)
在本次项目中,我们需要制作一个简易地图,数据结构涉及到图论,这里我使用了邻接表。
根据题目要求分析:
1、学校地图的输入和修改涉及到邻接表的输入与修改。
2、求两个地点之间的最短路径涉及到单源最短路径问题。
3、给定起点,生成遍历学校所有地点的方案涉及到深度优先搜索和广度优先搜索问题。
综上所述,大作业涉及到的数据结构有邻接表、栈、队列,算法有迪杰斯特拉算法,深度优先搜索算法、广度优先搜索算法。
数据结构:
1、栈
// 封装栈类
var Stack = function(){
var items = []
// 入栈
this.push = function(element){
items.push(element)
}
// 出栈
this.pop = function(){
return items.pop()
}
// 检查栈顶
this.peek = function(){
return items[items.length-1]
}
// 检查栈是否为空
this.isEmpty = function(){
return items.length == 0
}
// 清除栈
this.clear = function(){
items = []
}
// 获取栈大小
this.size = function(){
return items.length
}
}
2、队列
// 封装队列类
var Queue = function(){
var items = []
//入队
this.enqueue = function(element){
items.push(element)
}
// 出队
this.dequeue = function(){
return items.shift()
}
// 查看队列头
this.front = function(){
return items[0]
}
// 检查队列是否为空
this.isEmpty = function(){
return items.length ===0
}
// 队列大小
this.size = function(){
return items.length
}
}
3、邻接表
// 封装地图类(邻接表)
var Graph = function(){
// 存储顶点
var vertices = [];
// 存储边
var edges = {};
// 添加顶点
this.addVertex = function(v){
vertices.push(v);
edges[v] = [];
}
// 添加边
this.addEdge = function(start_point,end_point,weight){
edges[start_point].push({ vertex: end_point, weight: weight });
edges[end_point].push({ vertex: start_point, weight: weight });
}
// 输出图的顶点和边(测试用)
this.printGraph = function(){
for(var i=0;i<vertices.length;i++){
var vertex = vertices[i];
var edgeString = "";
for (var j = 0; j < edges[vertex].length; j++) {
edgeString += edges[vertex][j].vertex + "(" + edges[vertex][j].weight + ") ";
}
console.log(vertex + " -> " + edgeString);
}
}
}
算法:
1、深度优先搜索
// white 未发现
// grey 已发现未探索
// black 已探索
var initColor = function(){
var color = {}
for(var i=0;i<vertices.length;i++){
color[vertices[i]] = 'white'
}
return color;
}
// 深度优先搜索
var dfsVisit = function(u,color,visit_list){
color[u] = 'grey'
// if(callback){
// callback(u)
// }
visit_list.push(u);
var n = edges[u]
for(var i=0;i<n.length;i++){
var w = n[i].vertex
if(color[w]=='white'){
dfsVisit(w,color,visit_list)
}
}
color[u] = 'black'
}
this.dfs = function(v){
var visit_list = [];
var color = initColor()
dfsVisit(v,color,visit_list)
return visit_list;
}
2、广度优先搜索
// white 未发现
// grey 已发现未探索
// black 已探索
var initColor = function(){
var color = {}
for(var i=0;i<vertices.length;i++){
color[vertices[i]] = 'white'
}
return color;
}
// 广度优先搜索
this.bfs = function(v){
var visit_list = [];
// 初始化所有点为白色,即未发现
var color = initColor();
// 创建一个队列
var queue = new Queue
// 将输入的点入队
queue.enqueue(v);
// 循环,当队列不为空时
while(!queue.isEmpty()){
var now = queue.dequeue()
var bian = edges[now];
for(var i=0;i<bian.length;i++){
var w = bian[i].vertex;
if(color[w] === 'white'){
color[w] = 'grey'
queue.enqueue(w)
}
}
color[now] = 'black'
visit_list.push(now)
// 测试用
// if(callback){
// callback(now);
// }
}
return visit_list
}
3、迪杰斯特拉算法
// 迪杰斯特拉算法
this.dijkstra = function(start_point) {
// 初始化距离表,用于存储从起点到各顶点的最短路径
var dist = {};
// 初始化前驱点表,用于存储当前点最短路径的前驱点
var path = {};
// 用于存储待访问的顶点
var S = [];
// 初始化dist数组和path数组
for (var i = 0; i < vertices.length; i++) {
// 遍历图中每一个点
var vertex = vertices[i];
// dist数组初始都设为无穷大
dist[vertex] = Infinity;
// path数组初始都设为null
path[vertex] = null;
// 初始将点加入待处理队伍
S.push(vertex);
}
// 初始点到初始点距离为0
dist[start_point] = 0;
// 当S中还有待处理的点时循环
while (S.length > 0) {
// 找到距离起点最短的顶点
var currentVertex = S.reduce(function(minVertex, vertex) {
return dist[vertex] < dist[minVertex] ? vertex : minVertex;
}, S[0]);
// 移除该点
S = S.filter(function(vertex) {
return vertex !== currentVertex;
});
for (var j = 0; j < edges[currentVertex].length; j++) {
// 获取当前相邻点和边的权重信息
var neighbor = edges[currentVertex][j].vertex;
var edgeWeight = edges[currentVertex][j].weight;
// // 计算经过当前顶点到相邻顶点的总权重
var totalWeight = dist[currentVertex] + edgeWeight;
// 更新dist数组和path数组
if (totalWeight < dist[neighbor]) {
dist[neighbor] = totalWeight;
path[neighbor] = currentVertex;
}
}
}
return { dist: dist, path: path };
};
五、fastapi与ajax的联合使用(重点)
如何使得数据保存到数据库又能被灵活调用呢?
用一句话解释fastapi与ajax的联合使用就是:既能够使用fastapi模块在Python代码中访问数据库中的数据,传递到ajax中,也能够使用ajax使用Javascript代码传递数据到fastapi,进而保存到数据库。
举个例子:
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import Union
import psycopg2
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class map(BaseModel):
graph_name: Union[str,None] = None
new_place: Union[str,None] = None
x: Union[float,None] = None
y: Union[float,None] = None
start_point: Union[str,None] = None
end_point: Union[str,None] = None
weight: Union[int,None] = None
finished: Union[str,None] = None
@app.get('/')
async def welcome():
return {'message':"学校参观系统"}
# 初始化地图信息表
@nete.post('/init_tu_table')
async def init_tu_table(itt:map):
try:
conn = psycopg2.connect(
dbname = "map",
user = "postgres",
password = "0623",
host = "localhost",
port = "5432"
)
cur = conn.cursor()
# print(irnve.random_num)
cur.execute(f"create table if not exists map_tu_{itt.graph_name}(vertex_num int, edge_num int)")
conn.commit()
return {'message':'建立地图信息表成功!'}
except Exception as e:
print(e)
return {'message':"建立地图信息表失败!"}
finally:
cur.close()
conn.close()
if __name__ == '__main__':
import uvicorn
uvicorn.run(
app = "main:app",
host = "127.0.0.1",
port = 12348,
reload = True,
workers = 10
)
map.js
function init_tu_table(graph_name){
$.ajax({
url:"http://127.0.0.1:12348/init_tu_table",
type:"post",
contentType:"application/json",
data:JSON.stringify({
'graph_name':graph_name
}),
success:function(result){
console.log(result);
},
error:function (error) {
console.error(error);
}
});
}
在上述两个代码中,第一个是python代码,第二个是Javascript代码。
其中
上面两张图的工作流程是:
1、第二张图中将graph_name(地图名)传递到"http://127.0.0.1:12348/init_tu_table"这个地址中,数据会被第一张图所示@nete.post('/init_tu_table')装饰器下面的函数接收。
2、在数据接收,同时第一张图代码中连接数据库,根据传递过来的数据(graph_name)在数据库中创建一张表。
3、如果表创建成功,则返回'message':'建立地图信息表成功!',否则返回'message':"建立地图信息表失败!"(理所应当的,这个返回值也可以是数据库中的数据)
这样,数据就可以在前后端之间自由传递!
六、效果展示
数据结构与算法课设——学校导游参观系统 (Javascrip
七、完整代码(github)
GitHub - nete1108/School-tour-guide-system: 数据结构与算法课设——学校导游系统 (Javascript+Python)