铁大校园导游程序的设计(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 地图图片:

posted @ 2025-05-16 17:46  雨花阁  阅读(16)  评论(0)    收藏  举报