多源最短路径规划显示

88 阅读3分钟

<template>
  <div class="route-planner">
    <!-- 控制区域:路线类型选择 + 规划按钮 -->
    <div class="controls flex gap-8 mb-4">
      <el-button :disabled="isCalculating" :loading="isCalculating" type="primary" @click="planOptimalRoute">规划最优路径</el-button>

      <el-select style="width: 200px" v-model="routeType" placeholder="选择路线类型" size="default">
        <el-option label="步行" value="walk" />
        <el-option label="骑行" value="ride" />
        <el-option label="驾车" value="drive" />
      </el-select>
      <div>总里程:{{ totalMileage }} 米</div>
      <div>总耗时:{{ convertSecondsToMinutes(totalTime) }}</div>
    </div>
    <!-- 地图容器 -->
    <div v-show="mapShow" id="amap-container" ref="mapContainer" style="width: 100%; height: 500px"></div>
  </div>
</template>
<script lang="ts">
// 高德地图密钥
window._AMapSecurityConfig = {
  securityJsCode: 'a72add4d6543a8bb2551ed4544289633'
};
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AMapLoader from '@amap/amap-jsapi-loader'; // 引入高德地图
import { before } from 'node:test';
// @ts-ignore
// import tsp from 'tsp';
const isCalculating = ref(false); // 是否正在计算 控制buttonloading效果

const { proxy } = getCurrentInstance() as ComponentInternalInstance; //获取当前实例对象
//让按钮两秒内只能点一次
// function debounce(fn: () => void, delay: number) {
//   let timer: ReturnType<typeof setTimeout> | null = null;
//   return () => {
//     if (timer) {
//       clearTimeout(timer);
//     }
//     timer = setTimeout(() => {
//       fn();
//       timer = null;
//     }, delay);
//   };
// }

// 点位接口定义
interface Point {
  name: string;
  locationpoint: [number, number]; // [经度, 纬度]
}
const totalMileage = ref(0);
const totalTime = ref(0);
// 计算函数如果totalTime大于60,则转化为分钟,大于360,则转化为小时
const convertSecondsToMinutes = (seconds: number) => {
  if (seconds > 60) {
    return Math.floor(seconds / 60) + '分' + (seconds % 60) + '秒';
  } else {
    return seconds + '秒';
  }
};

// 接收 props
const props = defineProps<{
  points: Point[];
}>();

// 响应式数据
const mapShow = ref(false); // 是否显示地图
const routeType = ref<'walk' | 'ride' | 'drive'>('walk'); // 默认步行

// 地图相关引用
const mapContainer = ref<HTMLElement | null>(null); // 地图容器
let map: any = null; // 地图实例
let markers = ref<any[]>([]); // 地图标记点实例   ref 类型
let polylines = ref<any>([]); // 折线实例   ref 类型

/**
 * 初始化地图
 */
const initMap = () => {
  console.log('初始化地图', props.points); // 输出传入的点数据

  if (!mapContainer.value) return; //  确保地图容器已挂载

  AMapLoader.load({
    key: '你的key', // 替换为你的高德地图 Key
    version: '2.0', // 使用的 Maps SDK 版本
    plugins: ['AMap.Marker', 'AMap.Polyline', 'AMap.Walking', 'AMap.Riding', 'AMap.Driving'] // 需要使用的插件
  })
    .then((AMap) => {
      window.AMap = AMap; // 全局保存 AMap 实例
      console.log('AMap:', AMap); // 查看 AMap 对象上是否有 Walking/Riding/Driving 构造函数
      // 创建地图实例
      map = new AMap.Map(mapContainer.value as HTMLElement, {
        zoom: 13,
        center: props.points.length > 0 ? props.points[0].locationpoint : [116.397428, 39.90923] // 默认北京中心点
      });
    })
    .catch((e) => {
      console.error('地图加载失败:', e);
    });
};

/**
 * 清除地图上的覆盖物
 */
const clearOverlays = () => {
  // 清除 marker
  if (markers.value.length > 0) {
    markers.value.forEach((marker) => marker.setMap(null));
    markers.value = [];
  }

  // 清除 polyline
  if (polylines.value.length > 0) {
    polylines.value.forEach((line) => line.setMap(null));
    polylines.value = [];
  }
};

/**
 * 根据路线类型请求真实路径
 */
const planOptimalRoute = () => {
  totalMileage.value = 0;
  totalTime.value = 0;
  if (isCalculating.value) return; // 正在计算中
  isCalculating.value = true; // 标记正在计算中

  console.log('地图路径', props.points);
  mapShow.value = true; // 显示地图

  if (!map || props.points.length < 2) {
    proxy?.$modal.msgError('至少需要两个点才能规划路径');
    return;
  }

  clearOverlays(); // 清除旧的地图元素

  // const coords = props.points; // 类型为 Point[]
  // // 提取经纬度作为二维坐标数组
  // const coordinateArray = coords.map((p) => p.locationpoint); // [[lng, lat], ...]
  // 调用 tsp 得到访问顺序的索引数组
  // const orderedIndexes = tsp(coordinateArray);
  // const orderedPoints = orderedIndexes.map((i) => coords[i]);
  // const greedyPath = findShortestPathOrder(props.points); // 贪心初步路径
  // const pathPoints = twoOptOptimize(greedyPath); // 经过 2-opt 优化

  // const pathPoints = findShortestPathOrder(props.points); // 使用最短路径算法  贪心

  const optimizedPoints = sortPointsInCircle(props.points); // 使用最短路径算法  圆形
  const pathPoints = optimizedPoints.map((point) => point.locationpoint); // 获取路径点

  // 添加标记点
  props.points.forEach((point) => {
    const marker = new AMap.Marker({
      position: point.locationpoint,
      label: {
        content: point.name,
        offset: new AMap.Pixel(15, 20),
        show: true
      }
    });
    map.add(marker);
    markers.value.push(marker);
  });
  // const pointslist = [
  //   { name: '北京西站', locationpoint: [116.327245, 39.89044] },
  //   { name: '天安门', locationpoint: [116.407414, 39.91378] }
  // ];
  // pointslist.forEach((point) => {
  //   const marker = new AMap.Marker({
  //     position: point.locationpoint,
  //     label: {
  //       content: point.name,
  //       offset: new AMap.Pixel(15, 20),
  //       show: true
  //     }
  //   });
  //   map.add(marker);
  //   markers.value.push(marker);
  // });
  // 添加线路服务类型
  let serviceClass;
  switch (routeType.value) {
    case 'walk':
      serviceClass = new AMap.Walking();
      break;
    case 'ride':
      serviceClass = new AMap.Riding();
      break;
    case 'drive':
      serviceClass = new AMap.Driving();
      break;
  }
  // orderedPoints

  // 存储所有 polyline 路径
  // const allPolylines: any[] = [];

  // 遍历每个相邻点对,进行路径规划
  for (let i = 0; i < pathPoints.length - 1; i++) {
    const start = pathPoints[i];
    const end = pathPoints[i + 1];
    // 路径规划
    serviceClass.search(start, end, (status, result) => {
      console.log('result', result, status);
      totalMileage.value += result.routes[0]?.distance || 0;
      totalTime.value += result.routes[0]?.time || 0;
      if (status === 'complete' && result.routes?.length > 0) {
        let routePath;
        // 判断服务路径方式
        if (routeType.value === 'walk') {
          routePath = result.routes[0].steps.map((p) => p.path);
        }
        if (routeType.value === 'ride') {
          routePath = result.routes[0].rides.map((p) => p.path);
        }
        if (routeType.value === 'drive') {
          routePath = result.routes[0].steps.map((p) => p.path);
        }
        console.log('routePath', routePath);
        console.log('result', result);
        routePath = routePath.flatMap((subArray) => subArray.map((item) => [item.lng, item.lat]));
        console.log('routePath', routePath);
        const polylineInstance = new AMap.Polyline({
          path: routePath,
          strokeColor: '#FF33FF',
          strokeWeight: 6,
          lineJoin: 'round'
        });

        // 保存所有线段以便后续清除或管理
        // allPolylines.push(polylineInstance);
        polylines.value.push(polylineInstance);
        map.add(polylineInstance);
        map.setFitView();
        setTimeout(() => {
          isCalculating.value = false; // 2秒后恢复可点击状态
        }, 1000);
      } else {
        proxy?.$modal.msgError(`第 ${i + 1} 段路径规划失败`);
        setTimeout(() => {
          isCalculating.value = false; // 2秒后恢复可点击状态
        }, 1000);
      }
    });
  }
  // console.log('serviceClass', serviceClass);

  // serviceClass.search(pathPoints[0], pathPoints[pathPoints.length - 1], (status, result) => {
  //   console.log('路径规划结果:', status, result);
  //   if (status === 'complete' && result.routes?.length > 0) {
  //     // const path = result.routes[0].rides;
  //     const path = result.routes[0].steps.map((p) => new AMap.LngLat(p.start_location.lng, p.start_location.lat));
  //     polyline.value = new AMap.Polyline({
  //       path: path,
  //       strokeColor: '#000000',
  //       strokeWeight: 6,
  //       lineJoin: 'round'
  //     });

  //     map.add(polyline.value);
  //     map.setFitView();
  //   } else {
  //     proxy?.$modal.msgError('路径规划失败,请重试');
  //   }
  // });

  // serviceClass.search(
  //   // [
  //   //   [pathPoints[0][0], pathPoints[0][1]], // 起点
  //   //   [pathPoints[pathPoints.length - 1][0], pathPoints[pathPoints.length - 1][1]] // 终点
  //   // ],
  //   [
  //     [116.327245, 39.89044],
  //     [116.407414, 39.91378]
  //   ],
  //   (status: string, result: any) => {
  //     console.log(pathPoints[0], pathPoints[pathPoints.length - 1]);

  //     console.log('路径规划结果:', status, result); // 👈 关键日志!请贴出这部分内容!
  //     if (status === 'complete' && result.routes?.length > 0) {
  //       const path = result.routes[0].path;

  //       polyline.value = new AMap.Polyline({
  //         path: path,
  //         strokeColor: '#FF33FF',
  //         strokeWeight: 6,
  //         lineJoin: 'round'
  //       });

  //       map.add(polyline.value);
  //       map.setFitView();
  //     } else {
  //       proxy?.$modal.msgError('路径规划失败,请重试');
  //     }
  //   }
  // );
};

// onBeforeMount(() => {
//   window._AMapSecurityConfig = {
//     securityJsCode: '你的秘钥'
//   };
// });

// 计算两个经纬度之间的距离(单位:米)
function getDistance(latlng1: [number, number], latlng2: [number, number]) {
  const rad = (n: number) => (n * Math.PI) / 180;
  const R = 6371000; // 地球半径(米)
  const lat1 = latlng1[1],
    lng1 = latlng1[0];
  const lat2 = latlng2[1],
    lng2 = latlng2[0];

  const dLat = rad(lat2 - lat1);
  const dLng = rad(lng2 - lng1);

  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c;
}
// 环形算法
function sortPointsInCircle(points: Point[]): Point[] {
  if (points.length <= 2) return points;

  // 计算中心点
  const center = points.reduce(
    (acc, p) => {
      acc[0] += p.locationpoint[0];
      acc[1] += p.locationpoint[1];
      return acc;
    },
    [0, 0] as [number, number]
  );
  center[0] /= points.length;
  center[1] /= points.length;

  // 按照相对于中心点的角度排序
  return [...points].sort((a, b) => {
    const angleA = Math.atan2(a.locationpoint[1] - center[1], a.locationpoint[0] - center[0]);
    const angleB = Math.atan2(b.locationpoint[1] - center[1], b.locationpoint[0] - center[0]);
    return angleA - angleB;
  });
}
/**
 * 对现有路径进行 2-opt 优化,减少交叉路径
 */
function twoOptOptimize(points: Point[]): Point[] {
  const path = [...points];
  let improved = true;
  while (improved) {
    improved = false;
    for (let i = 1; i < path.length - 2; i++) {
      for (let j = i + 1; j < path.length - 1; j++) {
        const a1 = path[i - 1];
        const a2 = path[i];
        const b1 = path[j];
        const b2 = path[j + 1];

        const d1 = getDistance(a1.locationpoint, a2.locationpoint) + getDistance(b1.locationpoint, b2.locationpoint);
        const d2 = getDistance(a1.locationpoint, b1.locationpoint) + getDistance(a2.locationpoint, b2.locationpoint);

        if (d2 < d1) {
          // 反转 i 到 j 之间的路径
          path.splice(i, j - i + 1, ...path.slice(i, j + 1).reverse());
          improved = true;
        }
      }
    }
  }
  return path;
}
// 贪心算法生成最短路径顺序
function findShortestPathOrder(points: Point[]) {
  if (points.length <= 1) return points;

  const ordered: Point[] = [];
  const remaining = [...points];

  let currentPoint = remaining.shift()!;

  ordered.push(currentPoint);

  while (remaining.length > 0) {
    let closestIndex = 0;
    let minDistance = Infinity;

    for (let i = 0; i < remaining.length; i++) {
      const distance = getDistance(currentPoint.locationpoint, remaining[i].locationpoint);
      if (distance < minDistance) {
        minDistance = distance;
        closestIndex = i;
      }
    }

    currentPoint = remaining[closestIndex];
    ordered.push(currentPoint);
    remaining.splice(closestIndex, 1);
  }

  return ordered;
}

onBeforeUnmount(() => {
  clearOverlays();
});
onMounted(() => {
  initMap();
});
</script>

<style scoped>
.route-planner {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.controls {
  display: flex;
  align-items: center;
}
</style>

使用方法

      <div>
        <h1>巡检路径规划系统</h1>
        <!-- <RouteMap :points="insPoints" apiKey="S3DBZ-HYY63-JKC3T-RSZGW-TXN3O-HRBMH" /> -->
        <RouteMap :points="insPoints" />
      </div>

insPoints 是

const testPoints = [ { name: 'A', locationpoint: [116.4074, 39.9042] }, { name: 'B', locationpoint: [116.4174, 39.9042] }, { name: 'C', locationpoint: [116.4274, 39.9042] } ];

这样的格式。