铁大校园导游程序的设计(python)
【设计要求】基于石家庄铁道大学校园景点平面图,实现对校园景点的信息
管理和导游路径的查询管理等功能,并提供信息结果的图形化展示功能等。
【界面要求】要求图形界面实现。
1 项目结构:

2 数据库:
点击查看代码
/*
Navicat Premium Dump SQL
Source Server : localhost_3306_1
Source Server Type : MySQL
Source Server Version : 80037 (8.0.37)
Source Host : localhost:3306
Source Schema : stdee
Target Server Type : MySQL
Target Server Version : 80037 (8.0.37)
File Encoding : 65001
Date: 16/05/2025 17:40:55
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for edges
-- ----------------------------
DROP TABLE IF EXISTS `edges`;
CREATE TABLE `edges` (
`id` int NOT NULL AUTO_INCREMENT,
`start_node` int NULL DEFAULT NULL,
`end_node` int NULL DEFAULT NULL,
`distance` double NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `start_id`(`start_node` ASC) USING BTREE,
INDEX `end_id`(`end_node` ASC) USING BTREE,
UNIQUE INDEX `unique_edge_direction`(`start_node` ASC, `end_node` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 256 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for nodes
-- ----------------------------
DROP TABLE IF EXISTS `nodes`;
CREATE TABLE `nodes` (
`id` int NOT NULL AUTO_INCREMENT,
`x` int NOT NULL,
`y` int NOT NULL,
`type` enum('road','spot','virtual') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 416 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-- ----------------------------
-- Table structure for spots
-- ----------------------------
DROP TABLE IF EXISTS `spots`;
CREATE TABLE `spots` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL,
`x` int NULL DEFAULT NULL,
`y` int NULL DEFAULT NULL,
`width` int NULL DEFAULT NULL,
`height` int NULL DEFAULT NULL,
`node_id` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `node_id`(`node_id` ASC) USING BTREE,
CONSTRAINT `spots_ibfk_1` FOREIGN KEY (`node_id`) REFERENCES `nodes` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 35 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
3 项目代码:
点击查看代码
import heapq
from collections import defaultdict, namedtuple
import mysql.connector
from tkinter import *
from tkinter import ttk, messagebox
from PIL import Image, ImageTk
# 修改线段交点检测函数,考虑端点连接
def line_intersection(line1, line2):
(x1, y1), (x2, y2) = line1
(x3, y3), (x4, y4) = line2
# 先检查端点是否重合
if (x1, y1) == (x3, y3) or (x1, y1) == (x4, y4):
return x1, y1
if (x2, y2) == (x3, y3) or (x2, y2) == (x4, y4):
return x2, y2
denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if denominator == 0:
return None # 平行或重合
t_numerator = (x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)
u_numerator = (x1 - x3) * (y1 - y2) - (y1 - y3) * (x1 - x2)
t = t_numerator / denominator
u = -u_numerator / denominator
# 允许延长线交点但限制在合理范围内
if -1 <= t <= 2 and -1 <= u <= 2:
x = x1 + t * (x2 - x1)
y = y1 + t * (y2 - y1)
return x, y
return None
# 新增函数:计算两点距离
def distance(p1, p2):
return ((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2) ** 0.5
class Database:
def __init__(self):
"""初始化数据库连接"""
self.conn = mysql.connector.connect(
host="localhost",
port=3306,
user="",
password="",
database=""
)
self.cursor = self.conn.cursor()
# --------------------------
# 节点相关操作
# --------------------------
def add_node(self, x, y, node_type):
"""添加新节点"""
self.cursor.execute(
"INSERT INTO nodes (x, y, type) VALUES (%s, %s, %s)",
(x, y, node_type)
)
return self.cursor.lastrowid
def get_node_coordinates(self, node_id):
"""根据节点ID获取坐标"""
try:
self.cursor.execute("SELECT x, y FROM nodes WHERE id = %s", (node_id,))
result = self.cursor.fetchone()
return (result[0], result[1]) if result else (0, 0)
except Exception as e:
messagebox.showerror("Error", str(e))
return 0, 0
# --------------------------
# 边相关操作
# --------------------------
def add_edge(self, start, end, distance):
"""添加新边"""
# 检查重复边
self.cursor.execute(
"SELECT id FROM edges WHERE start_node = %s AND end_node = %s",
(start, end)
)
if self.cursor.fetchone():
messagebox.showwarning("警告", "该方向边已存在!")
return False
if start == end:
messagebox.showwarning("错误", "起点和终点不能相同!")
return False
try:
self.cursor.execute(
"INSERT INTO edges (start_node, end_node, distance) VALUES (%s, %s, %s)",
(start, end, distance)
)
self.conn.commit()
return True
except Exception as e:
messagebox.showerror("Error", str(e))
return False
def delete_edge(self, start, end):
"""删除指定边"""
try:
self.cursor.execute(
"DELETE FROM edges WHERE start_node = %s AND end_node = %s",
(start, end)
)
self.conn.commit()
return True
except Exception as e:
messagebox.showerror("Error", str(e))
return False
def get_edges(self):
"""获取所有边信息"""
self.cursor.execute("SELECT start_node, end_node, distance FROM edges")
return [(str(s), str(e), float(d)) for s, e, d in self.cursor.fetchall()]
def get_all_edges_with_coordinates(self):
"""获取带坐标信息的完整边数据"""
self.cursor.execute("""
SELECT
e.start_node,
e.end_node,
n1.x as start_x,
n1.y as start_y,
n2.x as end_x,
n2.y as end_y,
e.distance
FROM edges e
JOIN nodes n1 ON e.start_node = n1.id
JOIN nodes n2 ON e.end_node = n2.id
""")
return self.cursor.fetchall()
def get_road_edges(self):
"""获取道路类型的边数据"""
self.cursor.execute("""
SELECT
e.start_node,
e.end_node,
n1.x as start_x,
n1.y as start_y,
n2.x as end_x,
n2.y as end_y,
e.distance
FROM edges e
JOIN nodes n1 ON e.start_node = n1.id
JOIN nodes n2 ON e.end_node = n2.id
WHERE n1.type = 'road' AND n2.type = 'road'
""")
return self.cursor.fetchall()
# --------------------------
# 景点相关操作
# --------------------------
def add_spot(self, name, desc, x, y, width, height):
"""添加新景点"""
try:
# 创建关联节点
node_id = self.add_node(x, y, 'spot')
self.cursor.execute(
"""INSERT INTO spots
(name, description, x, y, width, height, node_id)
VALUES (%s, %s, %s, %s, %s, %s, %s)""",
(name, desc, x, y, width, height, node_id)
)
self.conn.commit()
return True
except Exception as e:
messagebox.showerror("Error", f"数据库错误: {str(e)}")
self.conn.rollback()
return False
def get_spots(self):
"""获取所有景点信息"""
self.cursor.execute("SELECT * FROM spots")
return self.cursor.fetchall()
def get_attraction_nodes(self):
"""获取所有景点名称(按字母排序)"""
self.cursor.execute("SELECT name FROM spots")
return sorted([str(s[0]) for s in self.cursor.fetchall()])
def delete_spot(self, name):
"""删除指定景点"""
try:
self.cursor.execute("DELETE FROM spots WHERE name = %s", (name,))
self.conn.commit()
return True
except Exception as e:
messagebox.showerror("Error", str(e))
return False
def update_spot(self, old_name, new_name, desc, x, y, width, height):
"""更新景点信息"""
try:
self.cursor.execute(
"""UPDATE spots
SET name=%s, description=%s, x=%s, y=%s, width=%s, height=%s
WHERE name=%s""",
(new_name, desc, x, y, width, height, old_name)
)
self.conn.commit()
return True
except Exception as e:
messagebox.showerror("Error", str(e))
return False
class CampusGuideApp:
def __init__(self, master):
self.master = master
self.db = Database()
self.zoom_scale = 1.0
self.highlight_rect = None
self.setup_ui()
self.load_data()
self.draw_map()
Button(self.control_frame, text="管理道路",
command=self.open_road_management).pack()
self.zoom_scale = 1.0
self.drag_data = {"x": 0, "y": 0, "item": None}
self.image_ref = None
self.canvas.bind("<MouseWheel>", self.zoom_handler)
self.canvas.bind("<ButtonPress-1>", self.start_drag)
self.canvas.bind("<B1-Motion>", self.on_drag)
self.selection_rect = None
self.selection_start = None
self.selecting_mode = False
self.management_window = None
self.highlight_rect = None # 新增:用于保存当前高亮矩形引用
self.start_combo['values'] = self.attraction_names # 仅景点
self.end_combo['values'] = self.attraction_names # 仅景点
def open_road_management(self):
ManagementRoadWindow(self.master, self.db, self)
def enable_selection_mode(self):
if self.management_window:
self.management_window.grab_release()
self.selecting_mode = True
self.canvas.unbind("<ButtonPress-1>")
self.canvas.unbind("<B1-Motion>")
self.canvas.bind("<Button-1>", self.start_selection)
self.canvas.bind("<B1-Motion>", self.update_selection)
self.canvas.bind("<ButtonRelease-1>", self.end_selection)
self.canvas.config(cursor="crosshair")
def highlight_spot_area(self, x, y, width, height):
"""在地图上高亮显示指定区域"""
# 清除旧的高亮
if self.highlight_rect:
self.canvas.delete(self.highlight_rect)
# 应用当前缩放比例
scaled_x = x * self.zoom_scale
scaled_y = y * self.zoom_scale
scaled_width = width * self.zoom_scale
scaled_height = height * self.zoom_scale
# 绘制半透明矩形
self.highlight_rect = self.canvas.create_rectangle(
scaled_x - scaled_width / 2,
scaled_y - scaled_height / 2,
scaled_x + scaled_width / 2,
scaled_y + scaled_height / 2,
outline="red",
width=2,
fill="", # 透明填充
dash=(4, 4),
tags="highlight"
)
def clear_highlight(self):
"""清除高亮显示"""
if self.highlight_rect:
self.canvas.delete(self.highlight_rect)
self.highlight_rect = None
def disable_selection_mode(self):
self.selecting_mode = False
self.canvas.unbind("<Button-1>")
self.canvas.unbind("<B1-Motion>")
self.canvas.unbind("<ButtonRelease-1>")
self.canvas.bind("<ButtonPress-1>", self.start_drag) # 新增
self.canvas.bind("<B1-Motion>", self.on_drag) # 新增
self.canvas.config(cursor="")
if self.selection_rect:
self.canvas.delete(self.selection_rect)
self.selection_rect = None
def start_selection(self, event):
self.selection_start = (
self.canvas.canvasx(event.x),
self.canvas.canvasy(event.y)
)
self.selection_rect = self.canvas.create_rectangle(
*self.selection_start, *self.selection_start,
outline="blue", width=2, dash=(4, 4)
)
def update_selection(self, event):
if self.selection_start and self.selection_rect:
end_x = self.canvas.canvasx(event.x)
end_y = self.canvas.canvasy(event.y)
self.canvas.coords(
self.selection_rect,
*self.selection_start, end_x, end_y
)
def validate_path(self, path):
edges = {(s1, s2) for s1, s2, _ in self.edges}
edges.update({(s2, s1) for s1, s2, _ in self.edges})
for i in range(len(path) - 1):
if (path[i], path[i + 1]) not in edges:
return False
return True
def end_selection(self, event):
end_x = self.canvas.canvasx(event.x)
end_y = self.canvas.canvasy(event.y)
x1 = self.selection_start[0] / self.zoom_scale
y1 = self.selection_start[1] / self.zoom_scale
x2 = end_x / self.zoom_scale
y2 = end_y / self.zoom_scale
center_x = int((x1 + x2) / 2)
center_y = int((y1 + y2) / 2)
width = abs(int(x2 - x1))
height = abs(int(y2 - y1))
max_x = self.image.width if hasattr(self, 'image') else 1000
max_y = self.image.height if hasattr(self, 'image') else 800
center_x = max(0, min(max_x, center_x))
center_y = max(0, min(max_y, center_y))
width = max(10, min(500, width))
height = max(10, min(500, height))
if (self.management_window and
self.management_window.winfo_exists()):
self.management_window.update_coordinates(center_x, center_y, width, height)
else:
self.management_window = None
self.disable_selection_mode()
if (self.management_window is not None and
self.management_window.winfo_exists()):
self.management_window.update_coordinates(center_x, center_y, width, height)
def open_management(self):
if self.management_window and self.management_window.winfo_exists():
self.management_window.lift()
return
self.management_window = ManagementWindow(
self.master, self.db, self, is_edit_mode=False)
def get_spot_node(self, name):
"""获取景点对应的节点ID"""
self.db.cursor.execute("SELECT node_id FROM spots WHERE name=%s", (name,))
result = self.db.cursor.fetchone()
return str(result[0]) if result else None
def setup_ui(self):
self.master.title("铁大校园导游系统")
self.master.geometry("1024x768")
self.control_frame = Frame(self.master, width=200)
self.control_frame.pack(side=LEFT, fill=Y)
self.canvas = Canvas(self.master, bg='white')
self.canvas.pack(side=RIGHT, expand=True, fill=BOTH)
# 所有子控件使用self.control_frame
Label(self.control_frame, text="起点:").pack()
self.start_combo = ttk.Combobox(self.control_frame)
self.start_combo.pack()
Label(self.control_frame, text="终点:").pack()
self.end_combo = ttk.Combobox(self.control_frame)
self.end_combo.pack()
Button(self.control_frame, text="查询路径", command=self.query_path).pack(pady=10)
Button(self.control_frame, text="添加景点", command=self.open_management).pack()
search_frame = Frame(self.control_frame) # 改为实例变量的子组件
search_frame.pack(pady=10)
Label(search_frame, text="景点查询:").pack(side=LEFT)
self.search_entry = Entry(search_frame, width=15)
self.search_entry.pack(side=LEFT, padx=5)
Button(search_frame, text="搜索", command=self.search_spot).pack(side=LEFT)
def zoom_handler(self, event):
if not self.selecting_mode:
scale_factor = 1.1 if event.delta > 0 else 0.9
self.zoom_scale = max(0.5, min(3.0, self.zoom_scale * scale_factor))
self.redraw_map()
def search_spot(self):
keyword = self.search_entry.get().strip()
if not keyword:
messagebox.showwarning("提示", "请输入要查询的景点名称")
return
try:
# 执行模糊查询
self.db.cursor.execute("SELECT name FROM spots WHERE name LIKE %s", ('%' + keyword + '%',))
results = [row[0] for row in self.db.cursor.fetchall()]
if not results:
messagebox.showinfo("提示", "未找到相关景点")
return
# 显示查询结果窗口
self.display_search_results(results)
except Exception as e:
messagebox.showerror("错误", str(e))
def display_search_results(self, results):
result_win = Toplevel(self.master)
result_win.transient(self.master) # 设置为子窗口
result_win.grab_set() # 获取焦点
result_win.title("查询结果")
Label(result_win, text="找到以下景点:", font=("微软雅黑", 12)).pack(pady=5)
list_frame = Frame(result_win)
list_frame.pack(padx=10, pady=10)
for name in results:
btn = Button(list_frame,
text=name,
width=20,
command=lambda n=name: self.show_selected_spot(n, result_win))
btn.pack(pady=2)
# 确保窗口显示在最前
result_win.lift()
result_win.focus_force()
def show_selected_spot(self, name, window):
window.destroy()
self.show_spot_info(name)
def start_drag(self, event):
if not self.selecting_mode:
x = self.canvas.canvasx(event.x) / self.zoom_scale
y = self.canvas.canvasy(event.y) / self.zoom_scale
for name, (spot_x, spot_y, width, height) in self.spots.items():
x1 = spot_x - width / 2
y1 = spot_y - height / 2
x2 = spot_x + width / 2
y2 = spot_y + height / 2
if x1 <= x <= x2 and y1 <= y <= y2:
self.show_spot_info(name)
return
self.drag_data["x"] = event.x
self.drag_data["y"] = event.y
self.canvas.scan_mark(event.x, event.y)
def on_drag(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
RoadSegment = namedtuple('RoadSegment', ['start_x', 'start_y', 'end_x', 'end_y', 'start_node', 'end_node'])
def load_data(self):
"""修改数据加载方式"""
self.spots = {spot[1]: (spot[3], spot[4], spot[5], spot[6]) for spot in self.db.get_spots()}
self.edges = self.db.get_edges()
self.attraction_names = self.db.get_attraction_nodes()
# 仅景点名称
# 新增:加载道路段数据
self.road_segments = []
for edge in self.db.get_road_edges():
self.road_segments.append(
self.RoadSegment(
start_x=edge[2],
start_y=edge[3],
end_x=edge[4],
end_y=edge[5],
start_node=str(edge[0]),
end_node=str(edge[1])
)
)
# 更新下拉框选项
self.start_combo['values'] = self.attraction_names
self.end_combo['values'] = self.attraction_names
# 检查当前选择是否有效
current_start = self.start_combo.get()
if current_start not in self.attraction_names:
self.start_combo.set('')
current_end = self.end_combo.get()
if current_end not in self.attraction_names:
self.end_combo.set('')
def find_nearest_road_point(self, x, y):
"""找到距离给定点最近的道路投影点"""
min_distance = float('inf')
nearest_point = None
nearest_segment = None
for segment in self.road_segments:
# 计算点到线段的投影
proj_point, distance = self.point_to_segment_projection(
x, y,
segment.start_x, segment.start_y,
segment.end_x, segment.end_y
)
if distance < min_distance:
min_distance = distance
nearest_point = proj_point
nearest_segment = segment
return nearest_point, nearest_segment
@staticmethod
def point_to_segment_projection(px, py, x1, y1, x2, y2):
"""计算点到线段的最近投影点和距离"""
dx = x2 - x1
dy = y2 - y1
segment_length_sq = dx * dx + dy * dy
if segment_length_sq == 0:
return (x1, y1), ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5
t = ((px - x1) * dx + (py - y1) * dy) / segment_length_sq
t = max(0, min(1, t))
nearest_x = x1 + t * dx
nearest_y = y1 + t * dy
distance = ((px - nearest_x) ** 2 + (py - nearest_y) ** 2) ** 0.5
return (nearest_x, nearest_y), distance
def redraw_map(self):
self.canvas.delete("all")
# 重绘地图图片
if hasattr(self, 'image'):
new_width = int(self.image.width * self.zoom_scale)
new_height = int(self.image.height * self.zoom_scale)
resized_img = self.image.resize((new_width, new_height), Image.Resampling.LANCZOS)
self.photo = ImageTk.PhotoImage(resized_img)
self.image_ref = self.photo
self.canvas.create_image(0, 0, anchor=NW, image=self.photo)
# 重绘所有道路
for start_node, end_node, _ in self.db.get_edges():
start_x, start_y = self.db.get_node_coordinates(start_node)
end_x, end_y = self.db.get_node_coordinates(end_node)
self.canvas.create_line(
start_x * self.zoom_scale,
start_y * self.zoom_scale,
end_x * self.zoom_scale,
end_y * self.zoom_scale,
fill='gray',
width=2,
tags="road"
)
# 重绘所有景点
for name, (x, y, width, height) in self.spots.items():
self.canvas.create_oval(
(x - 5) * self.zoom_scale,
(y - 5) * self.zoom_scale,
(x + 5) * self.zoom_scale,
(y + 5) * self.zoom_scale,
fill='blue',
tags="spot"
)
# 重绘高亮区域
if self.highlight_rect:
coords = self.canvas.coords(self.highlight_rect)
# 检查坐标有效性
if len(coords) == 4:
self.canvas.delete(self.highlight_rect)
self.highlight_rect = self.canvas.create_rectangle(
*coords,
outline="red",
width=2,
dash=(4, 4)
)
else:
# 坐标无效时清除高亮引用
self.highlight_rect = None
# 修改draw_map方法(精简为仅初始化时调用)
def draw_map(self):
try:
self.image = Image.open("campus_map.jpg").convert("RGB")
self.redraw_map() # 改为调用redraw_map
except FileNotFoundError:
self.canvas.create_text(400, 300, text="地图文件未找到", fill='red')
def show_spot_info(self, name):
self.db.cursor.execute("SELECT description,x,y,width,height FROM spots WHERE name=%s", (name,))
result = self.db.cursor.fetchone()
desc, x, y, width, height = result
info_win = Toplevel(self.master)
info_win.transient(self.master) # 设置为子窗口
info_win.grab_set() # 获取焦点
info_win.title("景点信息")
Label(info_win, text="景点名称:", font=("微软雅黑", 12)).grid(row=0, column=0, sticky=W)
name_label = Label(info_win, text=name, font=("微软雅黑", 12, "bold"))
name_label.grid(row=0, column=1)
Label(info_win, text="景点描述:", font=("微软雅黑", 12)).grid(row=1, column=0, sticky=W)
desc_label = Label(
info_win,
text=desc,
font=("微软雅黑", 10),
wraplength=300,
justify=LEFT,
anchor="w",
padx=5, pady=5
)
desc_label.grid(row=1, column=1, sticky="w")
btn_frame = Frame(info_win)
btn_frame.grid(row=3, columnspan=2, pady=10)
Button(btn_frame, text="删除景点",
command=lambda: self.delete_spot_action(name, info_win)).pack(side=LEFT, padx=5)
Button(btn_frame, text="修改信息",
command=lambda: self.open_edit_window(name, info_win)).pack(side=LEFT, padx=5)
info_win.lift()
info_win.focus_force()
def query_path(self):
start = self.start_combo.get()
end = self.end_combo.get()
# 添加类型校验
if not isinstance(start, str) or not isinstance(end, str):
messagebox.showwarning("错误", "无效的输入类型")
return
result = self.shortest_path(start, end)
# 明确类型检查
if not isinstance(result, dict):
messagebox.showwarning("错误", "无效的返回类型")
return
if 'error' in result:
messagebox.showinfo("提示", result['error'])
return
# 添加类型断言确保数据格式
try:
assert isinstance(result['node_path'], list)
assert all(isinstance(n, str) for n in result['node_path'])
assert isinstance(result['temp_coords'], dict)
except AssertionError:
messagebox.showerror("错误", "返回数据格式异常")
return
self.highlight_path(result['node_path'], result['temp_coords'])
messagebox.showinfo("路径详情",
f"最优路径: {' -> '.join(result['readable_path'])}\n总距离: {result['distance']:.2f}米")
def get_edge_distance(self, start, end):
for s1, s2, d in self.edges:
if (s1 == start and s2 == end) or (s2 == start and s1 == end):
return d
return 0
def build_graph(self):
graph = defaultdict(dict)
# 添加原始道路连接
for start, end, dist in self.edges:
graph[start][end] = dist
graph[end][start] = dist
# 为每个景点添加动态连接
for spot_name in self.spots.keys():
spot_node = self.get_spot_node(spot_name)
x, y, _, _ = self.spots[spot_name]
# 找到最近道路点和所属线段
(rx, ry), segment = self.find_nearest_road_point(x, y)
# 创建虚拟节点(存储到临时数据库)
virtual_node = self.db.add_node(rx, ry, 'virtual')
# 景点的虚拟节点到道路的距离(单位:米)
spot_to_road_dist = distance((x, y), (rx, ry)) * 0.5 # 假设0.5像素=1米
# 连接景点到虚拟节点
graph[spot_node][virtual_node] = spot_to_road_dist
graph[virtual_node][spot_node] = spot_to_road_dist
# 连接虚拟节点到道路两端
seg_start = segment.start_node
seg_end = segment.end_node
seg_length = ((segment.end_x - segment.start_x) ** 2 +
(segment.end_y - segment.start_y) ** 2) ** 0.5 * 0.5
# 计算虚拟节点到两端的距离
to_start = ((rx - segment.start_x) ** 2 +
(ry - segment.start_y) ** 2) ** 0.5 * 0.5
to_end = ((rx - segment.end_x) ** 2 +
(ry - segment.end_y) ** 2) ** 0.5 * 0.5
graph[virtual_node][seg_start] = to_start
graph[seg_start][virtual_node] = to_start
graph[virtual_node][seg_end] = to_end
graph[seg_end][virtual_node] = to_end
return graph
def shortest_path(self, start_spot: str, end_spot: str) -> dict:
"""改进的路径查询方法,仅计算道路部分的距离"""
global readable_path, full_path, road_distance
try:
# 获取起点和终点的节点ID
start_node = str(self.get_spot_node(start_spot))
end_node = str(self.get_spot_node(end_spot))
# 获取起点和终点的坐标
start_x, start_y, _, _ = self.spots[start_spot]
end_x, end_y, _, _ = self.spots[end_spot]
# 找到最近的投影点和对应线段
(start_proj_x, start_proj_y), start_segment = self.find_nearest_road_point(start_x, start_y)
(end_proj_x, end_proj_y), end_segment = self.find_nearest_road_point(end_x, end_y)
# 创建临时节点并保存坐标
start_proj_id = self.db.add_node(start_proj_x, start_proj_y, 'road')
end_proj_id = self.db.add_node(end_proj_x, end_proj_y, 'road')
temp_nodes = {
str(start_proj_id): (start_proj_x, start_proj_y),
str(end_proj_id): (end_proj_x, end_proj_y)
}
# 获取线段端点信息
segA_start = str(start_segment.start_node)
segA_end = str(start_segment.end_node)
segB_start = str(end_segment.start_node)
segB_end = str(end_segment.end_node)
# 计算投影点到端点的距离
segA_start_x, segA_start_y = self.db.get_node_coordinates(segA_start)
segA_end_x, segA_end_y = self.db.get_node_coordinates(segA_end)
dist_to_segA_start = ((start_proj_x - segA_start_x) ** 2 + (start_proj_y - segA_start_y) ** 2) ** 0.5 * 0.5
dist_to_segA_end = ((start_proj_x - segA_end_x) ** 2 + (start_proj_y - segA_end_y) ** 2) ** 0.5 * 0.5
segB_start_x, segB_start_y = self.db.get_node_coordinates(segB_start)
segB_end_x, segB_end_y = self.db.get_node_coordinates(segB_end)
dist_to_segB_start = ((end_proj_x - segB_start_x) ** 2 + (end_proj_y - segB_start_y) ** 2) ** 0.5 * 0.5
dist_to_segB_end = ((end_proj_x - segB_end_x) ** 2 + (end_proj_y - segB_end_y) ** 2) ** 0.5 * 0.5
# 构建增强图
graph = defaultdict(dict)
# 添加原始道路边
for s, e, d in self.edges:
graph[s][e] = d
graph[e][s] = d
# 新增逻辑:检查是否在同一条道路线段上
same_segment = (
(start_segment.start_node == end_segment.start_node and
start_segment.end_node == end_segment.end_node) or
(start_segment.start_node == end_segment.end_node and
start_segment.end_node == end_segment.start_node)
)
# 如果投影点在同一条线段,直接连接两个临时节点
if same_segment:
# 计算实际距离
dx = end_proj_x - start_proj_x
dy = end_proj_y - start_proj_y
direct_distance = (dx ** 2 + dy ** 2) ** 0.5 * 0.5 # 单位转换
graph[str(start_proj_id)][str(end_proj_id)] = direct_distance
graph[str(end_proj_id)][str(start_proj_id)] = direct_distance
# 添加临时节点连接
graph[str(start_proj_id)][str(segA_start)] = dist_to_segA_start
graph[str(segA_start)][str(start_proj_id)] = dist_to_segA_start
graph[str(start_proj_id)][str(segA_end)] = dist_to_segA_end
graph[str(segA_end)][str(start_proj_id)] = dist_to_segA_end
graph[str(end_proj_id)][str(segB_start)] = dist_to_segB_start
graph[str(segB_start)][str(end_proj_id)] = dist_to_segB_start
graph[str(end_proj_id)][str(segB_end)] = dist_to_segB_end
graph[str(segB_end)][str(end_proj_id)] = dist_to_segB_end
# 添加起点和终点的距离连接
graph[str(start_node)][str(start_proj_id)] = 0
graph[str(start_proj_id)][str(start_node)] = 0
graph[str(end_proj_id)][str(end_node)] = 0
graph[str(end_node)][str(end_proj_id)] = 0
try:
# 执行Dijkstra算法
path = self.dijkstra(graph, str(start_proj_id), str(end_proj_id))
finally:
# 确保清理临时节点
self.db.cursor.execute("DELETE FROM nodes WHERE id IN (%s, %s)", (start_proj_id, end_proj_id))
self.db.conn.commit()
if isinstance(path, list):
# 构建完整路径节点列表
full_path = [str(start_node)] + path + [str(end_node)]
# 转换为可读路径
readable_path = []
for node in full_path:
if node == str(start_node):
readable_path.append(start_spot)
elif node == str(end_node):
readable_path.append(end_spot)
else:
self.db.cursor.execute("SELECT name FROM spots WHERE node_id = %s", (node,))
spot_name = self.db.cursor.fetchone()
readable_path.append(spot_name[0] if spot_name else f"道路节点{node}")
# 计算道路距离
road_distance = sum(graph[u][v] for u, v in zip(path[:-1], path[1:]))
if path is None:
return {'error': '路径不存在'}
return {
'readable_path': readable_path,
'node_path': full_path,
'distance': road_distance,
'temp_coords': temp_nodes
}
except Exception as e:
return {'error': f'路径计算错误: {str(e)}'}
def dijkstra(self, graph, start, end):
heap = [(0, start)]
visited = set()
prev_nodes = {start: None} # 使用字典记录前驱节点
distances = {start: 0}
while heap:
(cost, current) = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == end:
# 回溯路径
path = []
while current is not None:
path.append(current)
current = prev_nodes[current]
return path[::-1] # 反转路径
for neighbor, weight in graph[current].items():
new_cost = cost + weight
if neighbor not in distances or new_cost < distances[neighbor]:
distances[neighbor] = new_cost
prev_nodes[neighbor] = current
heapq.heappush(heap, (new_cost, neighbor))
return None # 路径不存在
def highlight_path(self, node_path, temp_coords):
"""改进的高亮方法,支持临时节点坐标"""
self.canvas.delete("path")
prev_point = None
for node in node_path:
# 获取坐标
if node in temp_coords:
x, y = temp_coords[node]
else:
self.db.cursor.execute("SELECT x, y FROM nodes WHERE id = %s", (int(node),))
res = self.db.cursor.fetchone()
if not res:
continue
x, y = res
# 转换到缩放坐标
scaled_x = x * self.zoom_scale
scaled_y = y * self.zoom_scale
if prev_point:
# 绘制线段
self.canvas.create_line(
prev_point[0], prev_point[1],
scaled_x, scaled_y,
fill='blue', width=3, arrow=LAST, tags="path"
)
prev_point = (scaled_x, scaled_y)
def delete_spot_action(self, name, window):
if messagebox.askyesno("确认", f"确定要删除景点 {name} 吗?"):
if self.db.delete_spot(name):
self.load_data() # 触发下拉框更新
messagebox.showinfo("成功", "景点已删除")
window.destroy()
def open_edit_window(self, old_name, parent_window):
self.db.cursor.execute("SELECT * FROM spots WHERE name=%s", (old_name,))
result = self.db.cursor.fetchone()
# 将新的管理窗口实例赋值给self.management_window
self.management_window = ManagementWindow(
self.master,
self.db,
self, # 这里传递的app_instance就是CampusGuideApp实例
is_edit_mode=True,
old_name=old_name,
init_data=(result[1], result[2], result[3], result[4], result[5], result[6]),
callback=lambda: [self.load_data(), self.redraw_map()] # 修改回调以包含重绘
)
class ManagementWindow(Toplevel):
def __init__(self, master, db, app_instance, is_edit_mode=False, old_name=None, init_data=None, callback=None):
super().__init__(master)
# 清除之前的高亮
app_instance.clear_highlight()
self.transient(master) # 设置为子窗口
self.grab_set() # 获取焦点
# 如果是编辑模式显示当前位置
if is_edit_mode and init_data:
x, y, width, height = init_data[2], init_data[3], init_data[4], init_data[5]
app_instance.highlight_spot_area(x, y, width, height)
self.db = db
self.app = app_instance
# 确保app实例知道当前管理窗口
self.app.management_window = self # 新增关键代码
self.is_edit_mode = is_edit_mode
self.old_name = old_name
self.callback = callback
self.protocol("WM_DELETE_WINDOW", self.on_close)
self.setup_ui()
self.lift()
self.focus_force()
if self.winfo_exists():
self.grab_set()
if init_data:
self.name_entry.insert(0, init_data[0])
self.desc_text.insert("1.0", init_data[1])
self.x_entry.insert(0, str(init_data[2]))
self.y_entry.insert(0, str(init_data[3]))
self.width_entry.insert(0, str(init_data[4]))
self.height_entry.insert(0, str(init_data[5]))
def on_close(self):
if self.app:
self.app.clear_highlight()
if self.app.management_window == self:
self.app.management_window = None
self.destroy()
def setup_ui(self):
Label(self, text="名称:").grid(row=0, column=0)
self.name_entry = Entry(self)
self.name_entry.grid(row=0, column=1)
Label(self, text="描述:").grid(row=1, column=0, sticky="nw") # 顶部对齐
self.desc_text = Text(self,
height=4, # 显示4行高度
width=30, # 30字符宽度
wrap=WORD, # 自动换行模式
font=("微软雅黑", 10),
padx=5, pady=5)
self.desc_text.grid(row=1, column=1, sticky="ew", padx=5)
self.x_entry = Entry(self)
self.y_entry = Entry(self)
self.width_entry = Entry(self)
self.height_entry = Entry(self)
Label(self, text="位置选择:").grid(row=2, column=0)
self.select_btn = Button(self, text="在地图上选择区域", command=self.start_map_selection)
self.select_btn.grid(row=2, column=1)
btn_text = "修改景点" if self.is_edit_mode else "添加景点"
Button(self, text=btn_text, command=self.save_action).grid(row=6, columnspan=2)
self.x_entry.grid_remove()
self.y_entry.grid_remove()
self.width_entry.grid_remove()
self.height_entry.grid_remove()
def save_action(self):
if self.is_edit_mode:
self.update_spot()
else:
self.add_spot()
def add_spot(self):
name = self.name_entry.get()
desc = self.desc_text.get("1.0", END).strip() # 获取全部文本并去除首尾空格
x = self.x_entry.get()
y = self.y_entry.get()
width = self.width_entry.get()
height = self.height_entry.get()
if not all([name, x, y, width, height]):
messagebox.showwarning("警告", "请填写所有字段")
return
try:
x = int(x)
y = int(y)
width = int(width)
height = int(height)
except ValueError:
messagebox.showerror("错误", "坐标和尺寸必须为整数")
return
if width <= 0 or height <= 0:
messagebox.showerror("错误", "宽度和高度必须大于0")
return
# 在保存按钮的点击事件中调用:
if self.db.add_spot(name, desc, x, y, width, height):
messagebox.showinfo("成功", "景点添加成功")
self.app.load_data() # 刷新数据
self.app.redraw_map() # 重绘地图
self.destroy() # 关闭管理窗口
else:
messagebox.showwarning("警告", "添加失败,请检查输入")
def update_spot(self):
new_name = self.name_entry.get()
desc = self.desc_text.get("1.0", END).strip()
x = self.x_entry.get()
y = self.y_entry.get()
width = self.width_entry.get()
height = self.height_entry.get()
if not all([new_name, x, y, width, height]):
messagebox.showwarning("警告", "请填写所有字段")
return
try:
x = int(x)
y = int(y)
width = int(width)
height = int(height)
except ValueError:
messagebox.showerror("错误", "坐标和尺寸必须为整数")
return
if self.db.update_spot(self.old_name, new_name, desc, x, y, width, height):
messagebox.showinfo("成功", "景点修改成功")
self.app.clear_highlight()
if self.callback:
self.callback() # 此时callback包含load_data和redraw_map
self.destroy() # 关闭管理窗口
def start_map_selection(self):
"""启动地图选择模式"""
self.grab_release()
self.app.enable_selection_mode()
self.select_btn.config(text="正在选择中...")
def update_coordinates(self, x, y, w, h):
"""安全更新坐标数据"""
if not self.winfo_exists():
return
# 使用安全方式更新控件
def safe_update(entry, value):
if entry.winfo_exists():
entry.delete(0, END)
entry.insert(0, str(value))
safe_update(self.x_entry, x)
safe_update(self.y_entry, y)
safe_update(self.width_entry, w)
safe_update(self.height_entry, h)
if self.select_btn.winfo_exists():
self.select_btn.config(text="重新选择区域")
self.app.highlight_spot_area(x, y, w, h)
class ManagementRoadWindow(Toplevel):
def __init__(self, master, db: Database, app):
super().__init__(master)
self.db = db
self.app = app
self.points = []
self.drawing_mode = False
self._setup_ui()
def _setup_ui(self):
"""初始化用户界面组件"""
Label(self, text="点击地图选择起点和终点").pack(pady=5)
self.draw_btn = Button(
self,
text="开始绘制道路",
command=self._toggle_drawing_mode
)
self.draw_btn.pack(pady=3)
Button(
self,
text="完成绘制",
command=self.destroy
).pack(pady=5)
def _toggle_drawing_mode(self):
"""切换道路绘制模式"""
if not self.drawing_mode:
self._enable_drawing_mode()
else:
self._disable_drawing_mode()
def _enable_drawing_mode(self):
"""进入道路绘制状态"""
self.drawing_mode = True
self.draw_btn.config(text="绘制中...", relief=SUNKEN)
self.app.canvas.bind("<Button-1>", self._handle_click)
self.app.canvas.config(cursor="crosshair")
self.points = []
def _disable_drawing_mode(self):
"""退出道路绘制状态"""
self.drawing_mode = False
self.draw_btn.config(text="开始绘制道路", relief=RAISED)
self.app.canvas.unbind("<Button-1>")
self.app.canvas.config(cursor="")
def _handle_click(self, event):
"""处理地图点击事件"""
if len(self.points) >= 2:
return
# 计算原始坐标(考虑画布缩放)
raw_x = self.app.canvas.canvasx(event.x) / self.app.zoom_scale
raw_y = self.app.canvas.canvasy(event.y) / self.app.zoom_scale
self.points.append((raw_x, raw_y))
# 更新按钮状态显示
self.draw_btn.config(text=f"已选{len(self.points)}/2个点")
if len(self.points) == 2:
self._create_road()
self.after(1000, self._disable_drawing_mode)
def _create_road(self):
"""创建新道路的核心逻辑"""
try:
# 坐标对齐到5像素网格
new_start = (
round(self.points[0][0] / 5) * 5,
round(self.points[0][1] / 5) * 5
)
new_end = (
round(self.points[1][0] / 5) * 5,
round(self.points[1][1] / 5) * 5
)
# 节点处理
start_id = self._get_or_create_node(new_start)
end_id = self._get_or_create_node(new_end)
# 交叉点检测
existing_edges = self.db.get_all_edges_with_coordinates()
intersections = self._detect_intersections(new_start, new_end, existing_edges)
# 生成路径节点
path_points = self._process_intersections(
new_start, start_id,
new_end, end_id,
intersections
)
# 创建最终道路边
self._create_aligned_edges(path_points)
# 更新界面
messagebox.showinfo("成功", "道路创建成功")
self.app.load_data()
self.app.redraw_map()
except Exception as e:
messagebox.showerror("错误", f"道路创建失败: {str(e)}")
self.db.conn.rollback()
finally:
self.points = []
self._disable_drawing_mode()
def _get_or_create_node(self, point: tuple) -> int:
"""获取或创建道路节点"""
x, y = point
self.db.cursor.execute("""
SELECT id FROM nodes
WHERE ABS(x - %s) <= 5 AND ABS(y - %s) <= 5
ORDER BY (POW(x-%s,2) + POW(y-%s,2))
LIMIT 1
""", (x, y, x, y))
return result[0] if (result := self.db.cursor.fetchone()) else self.db.add_node(x, y, 'road')
def _detect_intersections(self, start, end, existing_edges):
"""检测道路交叉点"""
intersections = []
new_line = (start, end)
for edge in existing_edges:
existing_line = ((edge[2], edge[3]), (edge[4], edge[5]))
if intersect := line_intersection(new_line, existing_line):
if self._is_valid_intersection(intersect, existing_line, new_line):
intersections.append((intersect, edge))
return intersections
def _is_valid_intersection(self, point, line1, line2, threshold=10):
"""验证交叉点有效性"""
return (self._point_near_segment(point, line1, threshold) and
self._point_near_segment(point, line2, threshold))
@staticmethod
def _point_near_segment(point, segment, threshold=10):
"""判断点是否在线段附近"""
(x1, y1), (x2, y2) = segment
px, py = point
proj, _ = CampusGuideApp.point_to_segment_projection(px, py, x1, y1, x2, y2)
return distance(proj, (px, py)) <= threshold
def _process_intersections(self, start, start_id, end, end_id, intersections):
"""处理交叉点生成路径节点"""
path_points = [(start, start_id)]
# 按距离排序交叉点
sorted_intersections = sorted(intersections, key=lambda x: distance(start, x[0]))
for intersect, edge in sorted_intersections:
node_x = round(intersect[0] / 5) * 5
node_y = round(intersect[1] / 5) * 5
node_id = self._get_or_create_node((node_x, node_y))
self._split_existing_edge(edge, node_id)
path_points.append(((node_x, node_y), node_id))
path_points.append((end, end_id))
return path_points
def _split_existing_edge(self, edge, node_id):
"""分割现有道路边"""
start_node, end_node, _, _, _, _, orig_len = edge
seg1_len = distance((edge[2], edge[3]), self.db.get_node_coordinates(node_id)) * 0.5
seg2_len = orig_len - seg1_len
# 更新数据库
self.db.delete_edge(start_node, end_node)
self.db.add_edge(start_node, node_id, seg1_len)
self.db.add_edge(node_id, end_node, seg2_len)
self.db.add_edge(node_id, start_node, seg1_len)
self.db.add_edge(end_node, node_id, seg2_len)
def _create_aligned_edges(self, path_points):
"""创建对齐后的道路边"""
for i in range(len(path_points) - 1):
(p1, id1), (p2, id2) = path_points[i], path_points[i+1]
dist = distance(p1, p2) * 0.5 # 单位转换
self.db.add_edge(id1, id2, dist)
self.db.add_edge(id2, id1, dist)
if __name__ == "__main__":
root = Tk()
app = CampusGuideApp(root)
root.mainloop()
4 地图图片:

浙公网安备 33010602011771号