基于 Leaflet 与 Dijkstra 算法的路网最短路径分析系统

admin基于 Leaflet 与 Dijkstra 算法的路网最短路径分析系统已关闭评论条评论 10 次浏览

一、项目概述

在现代城市规划、物流调度和导航应用中,路网最短路径分析是一项核心技术。本文将详细介绍如何使用 Leaflet 地图库和 Dijkstra 算法构建一个完整的路网最短路径分析系统,包括前端界面设计、路网数据处理和路径计算算法实现。

在线预览地址:http://devmodels.oss-cn-shenzhen.aliyuncs.com/devtest/liubofang/dijkstra/index.html

二、技术实现

  1. 地图初始化与路网数据处理
    • 使用 Leaflet 地图库加载 OpenStreetMap 底图
    • 自定义路网数据结构,使用 GeoJSON 格式存储道路信息
    • 实现了从 GeoJSON 数据构建网络图的算法,将路网抽象为节点和边的图结构
  2. 最短路径算法
    • 实现了 Dijkstra 算法计算路网中的最短路径
    • 针对路网特点进行了算法优化,提高了路径计算效率
    • 处理了单行道、转弯限制等实际路网中的复杂情况
  3. 界面设计与交互
    • 使用 Tailwind CSS 实现了现代化的 UI 设计
    • 设计了响应式布局,确保在不同设备上都有良好的显示效果
    • 添加了平滑过渡动画,提升用户体

三、经验总结

  1. 技术选型
    • Leaflet 是一个轻量级、功能强大的地图库,非常适合开发路网分析应用
    • Dijkstra 算法在小规模路网中表现良好,但在大规模路网中可能需要更高效的算法

四、核心算法

function dijkstra(graph, start, end) {
        const distances = {};
        const previous = {};
        const queue = [];

        // 初始化距离和前驱节点
        for (const node in graph) {
          distances[node] = Infinity;
          previous[node] = null;
          queue.push(node);
        }

        distances[start] = 0;

        while (queue.length > 0) {
          // 找到距离最小的节点
          let minDistance = Infinity;
          let minNode = null;
          let minIndex = -1;

          queue.forEach((node, index) => {
            if (distances[node] < minDistance) {
              minDistance = distances[node];
              minNode = node;
              minIndex = index;
            }
          });

          // 如果找不到可达节点,退出循环
          if (minNode === null) break;

          // 从队列中移除当前节点
          queue.splice(minIndex, 1);

          // 如果到达终点,结束算法
          if (minNode === end) break;

          // 更新相邻节点的距离
          for (const neighbor in graph[minNode]) {
            const distance =
              distances[minNode] + graph[minNode][neighbor].distance;

            if (distance < distances[neighbor]) {
              distances[neighbor] = distance;
              previous[neighbor] = minNode;
            }
          }
        }

        // 构建最短路径
        const path = [];
        let current = end;

        while (current !== null) {
          path.unshift(current);
          current = previous[current];
        }

        return {
          path,
          distance: distances[end],
        };
      }

五、完整代码

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Leaflet路网最短路径分析</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link
      href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css"
      rel="stylesheet"
    />

    <!-- Leaflet相关资源 -->
    <link
      rel="stylesheet"
      href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
      integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
      crossorigin=""
    />
    <script
      src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
      integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
      crossorigin=""
    ></script>

    <!-- 路由算法库 -->
    <script src="https://unpkg.com/osrm-text-instructions@6.8.0/dist/osrm-text-instructions.min.js"></script>

    <script>
      tailwind.config = {
        theme: {
          extend: {
            colors: {
              primary: "#3B82F6",
              secondary: "#10B981",
              accent: "#8B5CF6",
              dark: "#1F2937",
              light: "#F3F4F6",
            },
            fontFamily: {
              inter: ["Inter", "system-ui", "sans-serif"],
            },
          },
        },
      };
    </script>

    <style type="text/tailwindcss">
      @layer utilities {
        .content-auto {
          content-visibility: auto;
        }
        .map-height {
          height: calc(100vh - 4rem);
        }
        .sidebar-width {
          width: clamp(280px, 30%, 400px);
        }
        .transition-all-300 {
          transition: all 0.3s ease;
        }
      }
    </style>
  </head>
  <body class="font-inter bg-gray-50 text-gray-800">
    <!-- 顶部导航栏 -->
    <header class="bg-white shadow-md fixed w-full z-50">
      <div
        class="container mx-auto px-4 py-3 flex justify-between items-center"
      >
        <div class="flex items-center space-x-2">
          <i class="fa fa-map-o text-primary text-2xl"></i>
          <h1 class="text-xl font-bold text-primary">路网分析系统</h1>
        </div>
        <div class="flex items-center space-x-4">
          <button
            id="helpBtn"
            class="text-gray-600 hover:text-primary transition-all-300"
          >
            <i class="fa fa-question-circle text-lg"></i>
          </button>
          <button
            id="aboutBtn"
            class="text-gray-600 hover:text-primary transition-all-300"
          >
            <i class="fa fa-info-circle text-lg"></i>
          </button>
        </div>
      </div>
    </header>

    <!-- 主内容区 -->
    <main class="pt-16 flex flex-col md:flex-row">
      <!-- 左侧控制面板 -->
      <div
        id="sidebar"
        class="sidebar-width bg-white shadow-lg z-40 md:min-h-screen overflow-y-auto transition-all-300 transform -translate-x-full md:translate-x-0 fixed md:relative top-16 md:top-0 h-[calc(100vh-4rem)]"
      >
        <div class="p-4">
          <div class="flex justify-between items-center mb-4">
            <h2 class="text-lg font-semibold text-gray-800">路径规划</h2>
            <button
              id="closeSidebar"
              class="md:hidden text-gray-500 hover:text-gray-800"
            >
              <i class="fa fa-times"></i>
            </button>
          </div>

          <div class="space-y-4">
            <div class="bg-gray-50 p-3 rounded-lg">
              <div class="flex items-center mb-3">
                <div
                  class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center text-white mr-3"
                >
                  <i class="fa fa-map-marker"></i>
                </div>
                <div class="flex-1">
                  <label class="block text-sm font-medium text-gray-700 mb-1"
                    >起点</label
                  >
                  <div class="relative">
                    <input
                      type="text"
                      id="startPoint"
                      placeholder="点击地图选择起点"
                      class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
                    />
                    <button
                      id="clearStart"
                      class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
                    >
                      <i class="fa fa-times-circle"></i>
                    </button>
                  </div>
                </div>
              </div>

              <div class="flex items-center">
                <div
                  class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white mr-3"
                >
                  <i class="fa fa-flag"></i>
                </div>
                <div class="flex-1">
                  <label class="block text-sm font-medium text-gray-700 mb-1"
                    >终点</label
                  >
                  <div class="relative">
                    <input
                      type="text"
                      id="endPoint"
                      placeholder="点击地图选择终点"
                      class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
                    />
                    <button
                      id="clearEnd"
                      class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
                    >
                      <i class="fa fa-times-circle"></i>
                    </button>
                  </div>
                </div>
              </div>
            </div>

            <div class="flex space-x-2">
              <button
                id="findRouteBtn"
                class="flex-1 bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-md transition-all-300 flex items-center justify-center"
              >
                <i class="fa fa-road mr-2"></i>计算路径
              </button>
              <button
                id="clearRouteBtn"
                class="bg-gray-200 hover:bg-gray-300 text-gray-700 font-medium py-2 px-4 rounded-md transition-all-300"
              >
                <i class="fa fa-refresh"></i>
              </button>
            </div>

            <!-- 路径信息 -->
            <div id="routeInfo" class="hidden">
              <div class="bg-gray-50 p-3 rounded-lg border border-gray-200">
                <div class="flex justify-between items-center mb-2">
                  <h3 class="font-medium text-gray-800">路径信息</h3>
                  <span
                    id="routeDistance"
                    class="text-sm font-medium text-green-600"
                  ></span>
                </div>
                <div
                  id="routeInstructions"
                  class="space-y-2 max-h-64 overflow-y-auto text-sm"
                >
                  <!-- 路径指引将在这里动态生成 -->
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- 地图区域 -->
      <div class="flex-1 relative">
        <div id="map" class="w-full map-height"></div>

        <!-- 浮动按钮 -->
        <div class="absolute bottom-4 right-4 flex flex-col space-y-2 z-30">
          <button
            id="toggleSidebar"
            class="md:hidden bg-primary text-white w-10 h-10 rounded-full shadow-lg flex items-center justify-center hover:bg-primary/90 transition-all-300"
          >
            <i class="fa fa-bars"></i>
          </button>
          <button
            id="locateMe"
            class="bg-white text-primary w-10 h-10 rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-all-300"
          >
            <i class="fa fa-location-arrow"></i>
          </button>
          <button
            id="zoomToRoute"
            class="bg-white text-primary w-10 h-10 rounded-full shadow-lg flex items-center justify-center hover:bg-gray-50 transition-all-300 opacity-50 cursor-not-allowed"
          >
            <i class="fa fa-search-plus"></i>
          </button>
        </div>

        <!-- 帮助模态框 -->
        <div
          id="helpModal"
          class="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center hidden"
          style="z-index: 9999"
        >
          <div
            class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 transform transition-all duration-300 scale-95 opacity-0"
            id="modalContent"
          >
            <div class="p-6">
              <div class="flex justify-between items-center mb-4">
                <h3 class="text-lg font-semibold text-gray-800">使用帮助</h3>
                <button
                  id="closeHelpModal"
                  class="text-gray-500 hover:text-gray-700"
                >
                  <i class="fa fa-times"></i>
                </button>
              </div>
              <div class="space-y-3 text-gray-700">
                <p><strong>1. 选择起点和终点</strong></p>
                <p class="text-sm ml-4">
                  点击地图上的任意位置选择起点和终点,或者在搜索框中输入地址
                </p>

                <p><strong>2. 计算路径</strong></p>
                <p class="text-sm ml-4">
                  点击"计算路径"按钮,系统将在路网中寻找最短路径
                </p>

                <p><strong>3. 查看结果</strong></p>
                <p class="text-sm ml-4">
                  地图上会显示最短路径,左侧面板会显示路径距离和详细指引
                </p>

                <p><strong>4. 清除路径</strong></p>
                <p class="text-sm ml-4">
                  点击刷新按钮可以清除当前路径,重新选择起点和终点
                </p>
              </div>
              <div class="mt-6">
                <button
                  id="gotItBtn"
                  class="w-full bg-primary hover:bg-primary/90 text-white font-medium py-2 px-4 rounded-md transition-all-300"
                >
                  我知道了
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>

    <script>
      // 全局变量
      let map, networkGraph, startMarker, endMarker, routeLine;
      let startPoint = null,
        endPoint = null;
      let nodes = {}; // 存储路网节点
      let edges = []; // 存储路网边
      let roadNetwork = null; // 存储GeoJSON路网数据
      let coordinateToNode = {}; // 坐标到节点ID的映射

      // 初始化地图
      function initMap() {
        // 创建地图实例并设置中心点和缩放级别
        map = L.map("map").setView([39.90923, 116.397428], 13);

        // 添加底图图层
        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
          attribution:
            '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
        }).addTo(map);

        // 加载路网数据
        loadNetworkData();

        // 地图点击事件 - 选择起点和终点
        map.on("click", function (e) {
          if (!startPoint) {
            // 设置起点
            setStartPoint(e.latlng);
          } else if (!endPoint) {
            // 设置终点
            setEndPoint(e.latlng);
          }
        });

        // 初始化界面事件监听
        initEventListeners();

        // 显示帮助模态框
        setTimeout(() => {
          document.getElementById("helpBtn").click();
        }, 500);
      }

      // 加载路网数据
      async function loadNetworkData() {
        // 使用GeoJSON格式的路网数据

        await fetch("./bj.geojson")
          .then((response) => response.json())
          .then((data) => {
            roadNetwork = data;
            // 从GeoJSON构建网络图
            buildNetworkFromGeoJSON();

            // 在地图上绘制路网
            drawNetwork();
          });
      }

      // 从GeoJSON构建网络图
      function buildNetworkFromGeoJSON() {
        // 清空现有节点和边
        nodes = {};
        edges = [];
        coordinateToNode = {};

        // 为每个坐标点生成唯一的节点ID
        roadNetwork.features.forEach((feature) => {
          if (feature.geometry.type === "LineString") {
            const coords = feature.geometry.coordinates;

            // 为每个坐标点创建节点
            coords.forEach((coord) => {
              const coordKey = `${coord[0].toFixed(6)},${coord[1].toFixed(6)}`;

              // 如果这个坐标还没有对应的节点ID,创建一个新的
              if (!coordinateToNode[coordKey]) {
                const nodeId = `node_${
                  Object.keys(coordinateToNode).length + 1
                }`;
                coordinateToNode[coordKey] = nodeId;
                nodes[nodeId] = coord;
              }
            });
          }
        });

        // 处理GeoJSON中的每条道路,创建边
        roadNetwork.features.forEach((feature) => {
          const roadId = feature.properties.osm_id;
          const roadName = feature.properties.name;
          const roadLength = feature.properties.length || 100; // 默认长度100米
          const isOneWay = feature.properties.oneway || false; // 是否单行道

          if (feature.geometry.type === "LineString") {
            const coords = feature.geometry.coordinates;

            // 为相邻节点创建边
            for (let i = 0; i < coords.length - 1; i++) {
              const fromCoord = coords[i];
              const toCoord = coords[i + 1];

              const fromCoordKey = `${fromCoord[0].toFixed(
                6
              )},${fromCoord[1].toFixed(6)}`;
              const toCoordKey = `${toCoord[0].toFixed(6)},${toCoord[1].toFixed(
                6
              )}`;

              const fromNode = coordinateToNode[fromCoordKey];
              const toNode = coordinateToNode[toCoordKey];

              // 添加正向边
              edges.push([
                fromNode,
                toNode,
                roadLength / (coords.length - 1),
                roadName,
              ]);

              // 如果不是单行道,添加反向边
              if (!isOneWay) {
                edges.push([
                  toNode,
                  fromNode,
                  roadLength / (coords.length - 1),
                  roadName,
                ]);
              }
            }
          }
        });

        // 构建网络图
        buildNetworkGraph();
      }

      // 构建网络图
      function buildNetworkGraph() {
        networkGraph = {};

        // 初始化图结构
        for (const nodeId in nodes) {
          networkGraph[nodeId] = {};
        }

        // 添加边到图中
        edges.forEach((edge) => {
          const [from, to, distance, name] = edge;

          // 添加双向边
          networkGraph[from][to] = { distance, name };
          networkGraph[to][from] = { distance, name };
        });
      }

      // 在地图上绘制路网
      function drawNetwork() {
        // 绘制道路线段
        if (roadNetwork) {
          L.geoJSON(roadNetwork, {
            style: function (feature) {
              return {
                color: "#ff6600",
                weight: 3,
                opacity: 0.7,
                // dashArray: '5, 5',
                lineJoin: "round",
              };
            },
            onEachFeature: function (feature, layer) {
              layer.bindTooltip(feature.properties.name, {
                permanent: false,
                direction: "auto",
              });
            },
          }).addTo(map);
        }
      }

      // 设置起点
      function setStartPoint(latlng) {
        // 清除现有起点标记
        if (startMarker) {
          map.removeLayer(startMarker);
        }

        // 创建新的起点标记
        startPoint = latlng;
        startMarker = L.marker(latlng, {
          icon: L.divIcon({
            html: '<div class="w-6 h-6 bg-red-500 rounded-full flex items-center justify-center text-white shadow-lg"><i class="fa fa-map-marker"></i></div>',
            className: "custom-div-icon",
            iconSize: [30, 30],
            iconAnchor: [15, 30],
          }),
          draggable: true,
        }).addTo(map);

        // 更新输入框
        document.getElementById("startPoint").value = `(${latlng.lat.toFixed(
          6
        )}, ${latlng.lng.toFixed(6)})`;

        // 添加拖拽事件
        startMarker.on("dragend", function (e) {
          startPoint = e.target.getLatLng();
          document.getElementById(
            "startPoint"
          ).value = `(${startPoint.lat.toFixed(6)}, ${startPoint.lng.toFixed(
            6
          )})`;
        });

        // 启用查找路径按钮(如果起点和终点都已设置)
        updateRouteButtonState();
      }

      // 设置终点
      function setEndPoint(latlng) {
        // 清除现有终点标记
        if (endMarker) {
          map.removeLayer(endMarker);
        }

        // 创建新的终点标记
        endPoint = latlng;
        endMarker = L.marker(latlng, {
          icon: L.divIcon({
            html: '<div class="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center text-white shadow-lg"><i class="fa fa-flag"></i></div>',
            className: "custom-div-icon",
            iconSize: [30, 30],
            iconAnchor: [15, 30],
          }),
          draggable: true,
        }).addTo(map);

        // 更新输入框
        document.getElementById("endPoint").value = `(${latlng.lat.toFixed(
          6
        )}, ${latlng.lng.toFixed(6)})`;

        // 添加拖拽事件
        endMarker.on("dragend", function (e) {
          endPoint = e.target.getLatLng();
          document.getElementById("endPoint").value = `(${endPoint.lat.toFixed(
            6
          )}, ${endPoint.lng.toFixed(6)})`;
        });

        // 启用查找路径按钮(如果起点和终点都已设置)
        updateRouteButtonState();
      }

      // 更新路径按钮状态
      function updateRouteButtonState() {
        const findRouteBtn = document.getElementById("findRouteBtn");
        if (startPoint && endPoint) {
          findRouteBtn.removeAttribute("disabled");
          findRouteBtn.classList.remove("opacity-50", "cursor-not-allowed");
        } else {
          findRouteBtn.setAttribute("disabled", "true");
          findRouteBtn.classList.add("opacity-50", "cursor-not-allowed");
        }
      }

      // 寻找最近的路网节点
      function findNearestNode(latlng) {
        let nearestNode = null;
        let minDistance = Infinity;

        for (const nodeId in nodes) {
          const nodeLatLng = L.latLng(nodes[nodeId][1], nodes[nodeId][0]);
          const distance = latlng.distanceTo(nodeLatLng);

          if (distance < minDistance) {
            minDistance = distance;
            nearestNode = nodeId;
          }
        }

        return nearestNode;
      }

      // Dijkstra算法实现
      function dijkstra(graph, start, end) {
        const distances = {};
        const previous = {};
        const queue = [];

        // 初始化距离和前驱节点
        for (const node in graph) {
          distances[node] = Infinity;
          previous[node] = null;
          queue.push(node);
        }

        distances[start] = 0;

        while (queue.length > 0) {
          // 找到距离最小的节点
          let minDistance = Infinity;
          let minNode = null;
          let minIndex = -1;

          queue.forEach((node, index) => {
            if (distances[node] < minDistance) {
              minDistance = distances[node];
              minNode = node;
              minIndex = index;
            }
          });

          // 如果找不到可达节点,退出循环
          if (minNode === null) break;

          // 从队列中移除当前节点
          queue.splice(minIndex, 1);

          // 如果到达终点,结束算法
          if (minNode === end) break;

          // 更新相邻节点的距离
          for (const neighbor in graph[minNode]) {
            const distance =
              distances[minNode] + graph[minNode][neighbor].distance;

            if (distance < distances[neighbor]) {
              distances[neighbor] = distance;
              previous[neighbor] = minNode;
            }
          }
        }

        // 构建最短路径
        const path = [];
        let current = end;

        while (current !== null) {
          path.unshift(current);
          current = previous[current];
        }

        return {
          path,
          distance: distances[end],
        };
      }

      // 计算最短路径
      function calculateRoute() {
        if (!startPoint || !endPoint) return;

        // 找到起点和终点最近的路网节点
        const startNode = findNearestNode(startPoint);
        const endNode = findNearestNode(endPoint);

        if (!startNode || !endNode) {
          alert("无法找到最近的路网节点");
          return;
        }

        // 使用Dijkstra算法计算最短路径
        const result = dijkstra(networkGraph, startNode, endNode);

        if (result.path.length === 0 || result.distance === Infinity) {
          alert("无法找到从起点到终点的路径");
          return;
        }

        // 显示路径
        displayRoute(result.path, result.distance);
      }

      // 显示路径
      function displayRoute(path, totalDistance) {
        // 清除现有路径
        if (routeLine) {
          map.removeLayer(routeLine);
        }

        // 构建路径点数组
        const routePoints = [];

        // 添加起点
        routePoints.push(startPoint);

        // 添加路径中间点
        for (let i = 0; i < path.length - 1; i++) {
          const fromNode = path[i];
          const toNode = path[i + 1];

          const fromLatLng = L.latLng(nodes[fromNode][1], nodes[fromNode][0]);
          const toLatLng = L.latLng(nodes[toNode][1], nodes[toNode][0]);

          routePoints.push(fromLatLng);
          routePoints.push(toLatLng);
        }

        // 添加终点
        routePoints.push(endPoint);

        // 创建路径线
        routeLine = L.polyline(routePoints, {
          color: "#3B82F6",
          weight: 5,
          opacity: 0.8,
          lineCap: "round",
          lineJoin: "round",
          interactive: true,
        }).addTo(map);

        // 显示路径信息
        showRouteInfo(path, totalDistance);

        // 缩放地图以显示完整路径
        map.fitBounds(routeLine.getBounds(), { padding: [50, 50] });

        // 启用缩放至路径按钮
        const zoomToRouteBtn = document.getElementById("zoomToRoute");
        zoomToRouteBtn.classList.remove("opacity-50", "cursor-not-allowed");
        zoomToRouteBtn.removeAttribute("disabled");
      }

      // 显示路径信息
      function showRouteInfo(path, totalDistance) {
        const routeInfo = document.getElementById("routeInfo");
        const routeDistance = document.getElementById("routeDistance");
        const routeInstructions = document.getElementById("routeInstructions");

        // 显示距离信息
        routeDistance.textContent = `总距离: ${(totalDistance / 1000).toFixed(
          2
        )} 公里`;

        // 生成路径指引
        let instructionsHTML = "";

        // 添加起点说明
        instructionsHTML += `
        <div class="flex items-center p-2 bg-green-50 rounded">
          <div class="w-6 h-6 rounded-full bg-red-500 flex items-center justify-center text-white mr-3">
            <i class="fa fa-map-marker"></i>
          </div>
          <div>
            <p class="font-medium">起点</p>
            <p class="text-sm text-gray-600">从这里出发</p>
          </div>
        </div>
      `;

        // 添加路径说明
        let currentRoadName = null;
        let currentRoadDistance = 0;

        for (let i = 0; i < path.length - 1; i++) {
          const fromNode = path[i];
          const toNode = path[i + 1];
          const roadName = networkGraph[fromNode][toNode].name;
          const roadDistance = networkGraph[fromNode][toNode].distance;

          // 如果是同一条路,累计距离
          if (roadName === currentRoadName) {
            currentRoadDistance += roadDistance;
          } else {
            // 如果不是同一条路,添加指引
            if (currentRoadName !== null) {
              instructionsHTML += `
              <div class="flex items-center p-2 hover:bg-gray-100 rounded transition-all-300">
                <div class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-primary mr-3">
                  ${i}
                </div>
                <div>
                  <p class="font-medium">沿 ${currentRoadName} 行驶</p>
                  <p class="text-sm text-gray-600">${(
                    currentRoadDistance / 1000
                  ).toFixed(2)} 公里</p>
                </div>
              </div>
            `;
            }

            currentRoadName = roadName;
            currentRoadDistance = roadDistance;
          }
        }

        // 添加最后一条路的指引
        if (currentRoadName !== null) {
          instructionsHTML += `
          <div class="flex items-center p-2 hover:bg-gray-100 rounded transition-all-300">
            <div class="w-6 h-6 rounded-full bg-primary/20 flex items-center justify-center text-primary mr-3">
              ${path.length - 1}
            </div>
            <div>
              <p class="font-medium">沿 ${currentRoadName} 行驶</p>
              <p class="text-sm text-gray-600">${(
                currentRoadDistance / 1000
              ).toFixed(2)} 公里</p>
            </div>
          </div>
        `;
        }

        // 添加终点说明
        instructionsHTML += `
        <div class="flex items-center p-2 bg-blue-50 rounded">
          <div class="w-6 h-6 rounded-full bg-blue-500 flex items-center justify-center text-white mr-3">
            <i class="fa fa-flag"></i>
          </div>
          <div>
            <p class="font-medium">终点</p>
            <p class="text-sm text-gray-600">到达目的地</p>
          </div>
        </div>
      `;

        // 更新指引内容
        routeInstructions.innerHTML = instructionsHTML;

        // 显示路径信息面板
        routeInfo.classList.remove("hidden");

        // 确保侧边栏是打开的
        if (window.innerWidth < 768) {
          document
            .getElementById("sidebar")
            .classList.remove("-translate-x-full");
        }
      }

      // 清除路径
      function clearRoute() {
        // 清除起点
        if (startMarker) {
          map.removeLayer(startMarker);
          startMarker = null;
          startPoint = null;
          document.getElementById("startPoint").value = "";
        }

        // 清除终点
        if (endMarker) {
          map.removeLayer(endMarker);
          endMarker = null;
          endPoint = null;
          document.getElementById("endPoint").value = "";
        }

        // 清除路径
        if (routeLine) {
          map.removeLayer(routeLine);
          routeLine = null;
        }

        // 隐藏路径信息
        document.getElementById("routeInfo").classList.add("hidden");

        // 禁用查找路径按钮
        updateRouteButtonState();

        // 禁用缩放至路径按钮
        const zoomToRouteBtn = document.getElementById("zoomToRoute");
        zoomToRouteBtn.classList.add("opacity-50", "cursor-not-allowed");
        zoomToRouteBtn.setAttribute("disabled", "true");
      }

      // 初始化事件监听
      function initEventListeners() {
        // 计算路径按钮
        document
          .getElementById("findRouteBtn")
          .addEventListener("click", calculateRoute);

        // 清除路径按钮
        document
          .getElementById("clearRouteBtn")
          .addEventListener("click", clearRoute);

        // 清除起点按钮
        document
          .getElementById("clearStart")
          .addEventListener("click", function () {
            if (startMarker) {
              map.removeLayer(startMarker);
              startMarker = null;
              startPoint = null;
              document.getElementById("startPoint").value = "";
              updateRouteButtonState();
            }
          });

        // 清除终点按钮
        document
          .getElementById("clearEnd")
          .addEventListener("click", function () {
            if (endMarker) {
              map.removeLayer(endMarker);
              endMarker = null;
              endPoint = null;
              document.getElementById("endPoint").value = "";
              updateRouteButtonState();
            }
          });

        // 移动端侧边栏切换
        document
          .getElementById("toggleSidebar")
          .addEventListener("click", function () {
            const sidebar = document.getElementById("sidebar");
            sidebar.classList.toggle("-translate-x-full");
          });

        // 关闭侧边栏(移动端)
        document
          .getElementById("closeSidebar")
          .addEventListener("click", function () {
            document
              .getElementById("sidebar")
              .classList.add("-translate-x-full");
          });

        // 缩放至路径按钮
        document
          .getElementById("zoomToRoute")
          .addEventListener("click", function () {
            if (routeLine) {
              map.fitBounds(routeLine.getBounds(), { padding: [50, 50] });
            }
          });

        // 定位到当前位置
        document
          .getElementById("locateMe")
          .addEventListener("click", function () {
            map.locate({ setView: true, maxZoom: 16 });
          });

        map.on("locationfound", function (e) {
          if (startPoint === null) {
            setStartPoint(e.latlng);
          }
        });

        map.on("locationerror", function (e) {
          alert("无法获取当前位置,请确保您的浏览器允许位置访问。");
        });

        // 帮助按钮
        document
          .getElementById("helpBtn")
          .addEventListener("click", function () {
            const helpModal = document.getElementById("helpModal");
            const modalContent = document.getElementById("modalContent");

            helpModal.classList.remove("hidden");
            setTimeout(() => {
              modalContent.classList.remove("scale-95", "opacity-0");
              modalContent.classList.add("scale-100", "opacity-100");
            }, 10);
          });

        // 关闭帮助模态框
        document
          .getElementById("closeHelpModal")
          .addEventListener("click", closeHelpModal);
        document
          .getElementById("gotItBtn")
          .addEventListener("click", closeHelpModal);

        function closeHelpModal() {
          const helpModal = document.getElementById("helpModal");
          const modalContent = document.getElementById("modalContent");

          modalContent.classList.remove("scale-100", "opacity-100");
          modalContent.classList.add("scale-95", "opacity-0");

          setTimeout(() => {
            helpModal.classList.add("hidden");
          }, 300);
        }

        // 关于按钮
        document
          .getElementById("aboutBtn")
          .addEventListener("click", function () {
            alert("路网最短路径分析系统\n基于Leaflet和Dijkstra算法实现");
          });
      }

      // 页面加载完成后初始化地图
      document.addEventListener("DOMContentLoaded", initMap);
    </script>
  </body>
</html>

分类目录