<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] } ];
这样的格式。