663 lines
19 KiB
Vue
663 lines
19 KiB
Vue
<template>
|
||
<div>
|
||
<div class="home-header">
|
||
<img @click="drawStartPoint" src="@/assets/image/start.png" />
|
||
<img @click="drawEndPoint" src="@/assets/image/end.png" />
|
||
<img @click="drawViaPoint" src="@/assets/image/add.png" />
|
||
<img @click="drawAvoidPoint" src="@/assets/image/avoidP.png" />
|
||
<img @click="drawAvoidArea" src="@/assets/image/updown.png" />
|
||
<div @click="calculateShortestPath" class="sure">确定</div>
|
||
<!-- <div class="control-panel">
|
||
<button @click="drawStartPoint">绘制起点</button>
|
||
<button @click="drawEndPoint">绘制终点</button>
|
||
<button @click="drawViaPoint">绘制途径点</button>
|
||
<button @click="drawAvoidPoint">绘制避让点</button>
|
||
<button @click="drawAvoidArea">绘制避让区域</button>
|
||
<button @click="calculateShortestPath">计算最短路径</button>
|
||
<button @click="clear">清除所有</button>
|
||
</div> -->
|
||
</div>
|
||
<div class="home">
|
||
<div class="main-container">
|
||
<div class="control-panel">
|
||
<div class="title">参数</div>
|
||
<el-form
|
||
@submit.native.prevent="calculateShortestPath"
|
||
label-width="80px"
|
||
label-position="left"
|
||
size="small"
|
||
>
|
||
<el-form-item label="起点">
|
||
<el-input v-model="form.startPoint"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="终点">
|
||
<el-input v-model="form.endPoint"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="途径点">
|
||
<el-input v-model="form.viaPoints" placeholder=""></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="避让点">
|
||
<el-input v-model="form.avoidPoints" placeholder=""></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="避让区域">
|
||
<el-input
|
||
type="textarea"
|
||
v-model="form.avoidAreas"
|
||
:rows="4"
|
||
placeholder=""
|
||
></el-input>
|
||
</el-form-item>
|
||
<!-- <el-form-item>
|
||
<el-button type="primary" @click="calculateShortestPath">计算最短路径</el-button>
|
||
<el-button @click="clear">清除所有</el-button>
|
||
</el-form-item> -->
|
||
</el-form>
|
||
<div class="importJson">导入json文件</div>
|
||
</div>
|
||
<div class="control-panel">
|
||
<div class="title">机动属性</div>
|
||
<el-form
|
||
@submit.native.prevent="calculateShortestPath"
|
||
label-width="80px"
|
||
label-position="left"
|
||
size="small"
|
||
>
|
||
<el-form-item label="起点">
|
||
<el-input v-model="form.startPoint"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="终点">
|
||
<el-input v-model="form.endPoint"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="途径点">
|
||
<el-input v-model="form.viaPoints" placeholder=""></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="避让点">
|
||
<el-input v-model="form.avoidPoints" placeholder=""></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="避让区域">
|
||
<el-input
|
||
type="textarea"
|
||
v-model="form.avoidAreas"
|
||
:rows="4"
|
||
placeholder=""
|
||
></el-input>
|
||
</el-form-item>
|
||
<!-- <el-form-item>
|
||
<el-button type="primary" @click="calculateShortestPath">计算最短路径</el-button>
|
||
<el-button @click="clear">清除所有</el-button>
|
||
</el-form-item> -->
|
||
</el-form>
|
||
<div class="importJson">导入json文件</div>
|
||
</div>
|
||
</div>
|
||
<!-- <div id="map"></div> -->
|
||
<div id="mapbox"></div>
|
||
<div class="main-container">
|
||
<div class="control-panel"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import Header from './header/index.vue'
|
||
export default {
|
||
name: '',
|
||
components: {
|
||
Header,
|
||
},
|
||
data() {
|
||
return {
|
||
form: {},
|
||
viewer: null,
|
||
graphicLayer: null,
|
||
polygonZAM: null,
|
||
pointQD: null,
|
||
pointZD: null,
|
||
shortestPathLayer: null,
|
||
roadNetworkLayer: null,
|
||
roadNetworkGeoJSON: null,
|
||
mapOptions: null,
|
||
viaPoints: [], // 途径点
|
||
avoidPoints: [], // 避让点
|
||
avoidAreas: [], // 避让区域
|
||
roadNetworkGeoJSONBuild: null,
|
||
}
|
||
},
|
||
async mounted() {
|
||
await this.getMapOption()
|
||
this.initMap()
|
||
},
|
||
methods: {
|
||
async getMapOption() {
|
||
await fetch('./config/map.json')
|
||
.then((response) => {
|
||
return response.json()
|
||
})
|
||
.then((data) => {
|
||
this.mapOptions = data.map3d
|
||
})
|
||
.catch((error) => {})
|
||
},
|
||
async initMap() {
|
||
this.viewer = new window.mars3d.Map('map', this.mapOptions || {})
|
||
this.graphicLayer = new window.mars3d.layer.GraphicLayer()
|
||
this.viewer.addLayer(this.graphicLayer)
|
||
|
||
this.shortestPathLayer = new window.mars3d.layer.GraphicLayer()
|
||
this.viewer.addLayer(this.shortestPathLayer)
|
||
this.loadShapefile()
|
||
// 添加地图点击事件监听,用于结束绘制
|
||
this.viewer.on(mars3d.EventType.dblClick, (event) => {
|
||
// 如果正在绘制,点击地图可以结束绘制(除了绘制点)
|
||
this.graphicLayer.stopDraw()
|
||
})
|
||
},
|
||
clear() {
|
||
// 清除障碍面
|
||
if (this.polygonZAM) {
|
||
this.polygonZAM.remove()
|
||
this.polygonZAM = null
|
||
}
|
||
|
||
// 清除起点
|
||
if (this.pointQD) {
|
||
this.pointQD.remove()
|
||
this.pointQD = null
|
||
}
|
||
|
||
// 清除终点
|
||
if (this.pointZD) {
|
||
this.pointZD.remove()
|
||
this.pointZD = null
|
||
}
|
||
|
||
// 清除途径点
|
||
this.viaPoints.forEach((point) => {
|
||
point.remove()
|
||
})
|
||
this.viaPoints = []
|
||
|
||
// 清除避让点
|
||
this.avoidPoints.forEach((point) => {
|
||
point.remove()
|
||
})
|
||
this.avoidPoints = []
|
||
|
||
// 清除避让区域
|
||
this.avoidAreas.forEach((area) => {
|
||
area.remove()
|
||
})
|
||
this.avoidAreas = []
|
||
|
||
// 清除最短路径图层
|
||
this.shortestPathLayer.clear()
|
||
|
||
// 清除路网数据图层
|
||
// this.graphicLayer.clear();
|
||
},
|
||
async loadShapefile() {
|
||
try {
|
||
// 1. 读取本地 SHP(浏览器)
|
||
// const shpBuffer = await fetch('./config/map/data/dlfs_lines.zip').then(r => r.arrayBuffer());
|
||
// const shpJson = await shp(shpBuffer); // {features: [...]}
|
||
|
||
// 1. 读取本地 jeojson
|
||
const shpBuffer = await fetch('./config/map/data/dao.geojson')
|
||
.then((response) => {
|
||
return response.json()
|
||
})
|
||
.then((data) => {
|
||
return data
|
||
})
|
||
.catch((error) => {})
|
||
|
||
// 2. 处理 GeoJSON 数据
|
||
this.roadNetworkGeoJSON = shpBuffer
|
||
|
||
/// 3. 将 GeoJSON 数据添加到 graphicLayer
|
||
this.roadNetworkGeoJSON.features.forEach((feature) => {
|
||
const positions = feature.geometry.coordinates[0]
|
||
const graphic = new window.mars3d.graphic.PolylineEntity({
|
||
positions: positions,
|
||
style: {
|
||
color: '#FF0000',
|
||
width: 2,
|
||
outline: false,
|
||
},
|
||
})
|
||
this.graphicLayer.addGraphic(graphic)
|
||
})
|
||
this.roadNetworkGeoJSONBuild = this.buildGraph(this.roadNetworkGeoJSON)
|
||
} catch (error) {
|
||
console.error('加载 Shapefile 数据失败:', error)
|
||
}
|
||
},
|
||
async drawPolygon() {
|
||
if (this.polygonZAM) {
|
||
this.polygonZAM.remove()
|
||
this.polygonZAM = null
|
||
}
|
||
this.polygonZAM = await this.graphicLayer.startDraw({
|
||
type: 'polygon',
|
||
style: {
|
||
color: '#00ffff',
|
||
opacity: 0.4,
|
||
clampToGround: true,
|
||
outline: true,
|
||
outlineWidth: 1,
|
||
outlineColor: '#ffffff',
|
||
},
|
||
})
|
||
},
|
||
drawStartPoint() {
|
||
if (this.pointQD) {
|
||
this.pointQD.remove()
|
||
this.pointQD = null
|
||
}
|
||
this.graphicLayer.startDraw({
|
||
type: 'point',
|
||
style: {
|
||
pixelSize: 10,
|
||
color: 'red',
|
||
label: {
|
||
text: '起点',
|
||
font_size: 20,
|
||
color: '#ffffff',
|
||
outline: true,
|
||
outlineColor: '#000000',
|
||
pixelOffsetY: -20,
|
||
},
|
||
},
|
||
success: (graphic) => {
|
||
this.pointQD = graphic
|
||
this.graphicLayer.stopDraw()
|
||
},
|
||
})
|
||
},
|
||
drawEndPoint() {
|
||
if (this.pointZD) {
|
||
this.pointZD.remove()
|
||
this.pointZD = null
|
||
}
|
||
this.graphicLayer.startDraw({
|
||
type: 'point',
|
||
style: {
|
||
pixelSize: 10,
|
||
color: 'red',
|
||
label: {
|
||
text: '终点',
|
||
font_size: 20,
|
||
color: '#ffffff',
|
||
outline: true,
|
||
outlineColor: '#000000',
|
||
pixelOffsetY: -20,
|
||
},
|
||
},
|
||
success: (graphic) => {
|
||
this.pointZD = graphic
|
||
this.graphicLayer.stopDraw()
|
||
},
|
||
})
|
||
},
|
||
// 途径点
|
||
drawViaPoint() {
|
||
this.graphicLayer.startDraw({
|
||
type: 'point',
|
||
style: {
|
||
pixelSize: 10,
|
||
color: 'blue',
|
||
label: {
|
||
text: '途径点',
|
||
font_size: 20,
|
||
color: '#ffffff',
|
||
outline: true,
|
||
outlineColor: '#000000',
|
||
pixelOffsetY: -20,
|
||
},
|
||
},
|
||
success: (graphic) => {
|
||
this.viaPoints.push(graphic)
|
||
this.graphicLayer.stopDraw()
|
||
},
|
||
})
|
||
},
|
||
// 避让点
|
||
drawAvoidPoint() {
|
||
this.graphicLayer.startDraw({
|
||
type: 'point',
|
||
style: {
|
||
pixelSize: 10,
|
||
color: 'orange',
|
||
label: {
|
||
text: '避让点',
|
||
font_size: 20,
|
||
color: '#ffffff',
|
||
outline: true,
|
||
outlineColor: '#000000',
|
||
pixelOffsetY: -20,
|
||
},
|
||
},
|
||
success: (graphic) => {
|
||
this.avoidPoints.push(graphic)
|
||
this.graphicLayer.stopDraw()
|
||
},
|
||
})
|
||
},
|
||
// 避让区域
|
||
drawAvoidArea() {
|
||
this.graphicLayer.startDraw({
|
||
type: 'polygon',
|
||
drawEndEventType: window.mars3d.EventType.dblClick,
|
||
style: {
|
||
color: '#ff0000',
|
||
opacity: 0.4,
|
||
clampToGround: true,
|
||
outline: true,
|
||
outlineWidth: 1,
|
||
outlineColor: '#ffffff',
|
||
},
|
||
success: (graphic) => {
|
||
this.avoidAreas.push(graphic)
|
||
this.graphicLayer.stopDraw()
|
||
},
|
||
})
|
||
},
|
||
// 1. 构建路网拓扑图(经纬度坐标直接处理
|
||
buildGraph(geojson) {
|
||
const graph = {}
|
||
const nodeCoords = {} // 存储节点经纬度
|
||
|
||
geojson.features.forEach((road) => {
|
||
const startNode = road.properties.FNODE_
|
||
const endNode = road.properties.TNODE_
|
||
const coords = road.geometry.coordinates
|
||
|
||
// 记录节点经纬度(取首尾点)
|
||
if (!nodeCoords[startNode]) {
|
||
nodeCoords[startNode] = coords[0][0]
|
||
}
|
||
if (!nodeCoords[endNode]) {
|
||
nodeCoords[endNode] = coords[coords.length - 1][0]
|
||
}
|
||
// 计算边权重(距离,单位:米)
|
||
const distance =
|
||
turf.distance(
|
||
turf.point(coords[0][0]),
|
||
turf.point(coords[coords.length - 1][0]),
|
||
{units: 'kilometers'}
|
||
) * 1000
|
||
|
||
// 构建邻接表(双向图)
|
||
graph[startNode] = graph[startNode] || {}
|
||
graph[startNode][endNode] = distance
|
||
|
||
graph[endNode] = graph[endNode] || {}
|
||
graph[endNode][startNode] = distance
|
||
})
|
||
|
||
return {graph, nodeCoords}
|
||
},
|
||
// 2. 匹配最近路网节点(经纬度坐标)
|
||
findNearestNode(pointCoord, nodeCoords) {
|
||
let nearestNode = null
|
||
let minDist = 10000
|
||
|
||
for (const [node, coord] of Object.entries(nodeCoords)) {
|
||
const dist = turf.distance(turf.point(pointCoord), turf.point(coord), {
|
||
units: 'meters',
|
||
})
|
||
|
||
if (dist < minDist) {
|
||
minDist = dist
|
||
nearestNode = node
|
||
}
|
||
}
|
||
return nearestNode
|
||
},
|
||
// 3. 路径规划主函数(经纬度坐标输入) - 支持途径点 + 避让点/区域
|
||
async planRoute(
|
||
startCoord,
|
||
endCoord,
|
||
viaPoints = [],
|
||
avoidObstacles = null
|
||
) {
|
||
const {graph, nodeCoords} = this.roadNetworkGeoJSONBuild
|
||
|
||
// 按顺序组合点:起点 -> 途径点 -> 终点
|
||
const points =
|
||
viaPoints && viaPoints.length > 0
|
||
? [startCoord, viaPoints[0].geometry.coordinates, endCoord]
|
||
: [startCoord, endCoord]
|
||
const fullPath = []
|
||
|
||
for (let i = 0; i < points.length - 1; i++) {
|
||
const segmentStart = points[i]
|
||
const segmentEnd = points[i + 1]
|
||
|
||
// 匹配最近节点
|
||
const startNode = this.findNearestNode(segmentStart, nodeCoords)
|
||
const endNode = this.findNearestNode(segmentEnd, nodeCoords)
|
||
|
||
if (!startNode || !endNode) {
|
||
console.log('无法匹配到路网节点,请检查坐标是否在路网范围内')
|
||
continue
|
||
}
|
||
// 构建临时 graph
|
||
const tempGraph = JSON.parse(JSON.stringify(graph))
|
||
|
||
// 删除避让点节点
|
||
if (avoidObstacles) {
|
||
const obstacleNodes = []
|
||
for (const [node, coord] of Object.entries(nodeCoords)) {
|
||
const pt = turf.point(coord)
|
||
avoidObstacles.features.forEach((ob) => {
|
||
if (turf.booleanPointInPolygon(pt.geometry, ob.geometry))
|
||
obstacleNodes.push(node)
|
||
})
|
||
}
|
||
obstacleNodes.forEach((node) => delete tempGraph[node])
|
||
|
||
// 删除与避让区域相交的边
|
||
for (const [node, edges] of Object.entries(tempGraph)) {
|
||
for (const targetNode of Object.keys(edges)) {
|
||
if (!tempGraph[targetNode]) {
|
||
// 避让点节点已经删除
|
||
delete tempGraph[node][targetNode]
|
||
continue
|
||
}
|
||
const line = turf.lineString([
|
||
nodeCoords[node],
|
||
nodeCoords[targetNode],
|
||
])
|
||
avoidObstacles.features.forEach((area) => {
|
||
if (turf.booleanCrosses(line, area)) {
|
||
delete tempGraph[node][targetNode]
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
// Dijkstra 计算最短路径
|
||
const pathNodes = dijkstra.find_path(tempGraph, startNode, endNode)
|
||
if (!pathNodes || pathNodes.length === 0) {
|
||
console.log('未找到可行路径段:', i)
|
||
continue
|
||
}
|
||
|
||
// 生成路径几何
|
||
for (let j = 0; j < pathNodes.length - 1; j++) {
|
||
const currentNode = pathNodes[j]
|
||
const nextNode = pathNodes[j + 1]
|
||
const segment = this.roadNetworkGeoJSON.features.find(
|
||
(f) =>
|
||
(f.properties.FNODE_ == currentNode &&
|
||
f.properties.TNODE_ == nextNode) ||
|
||
(f.properties.FNODE_ == nextNode &&
|
||
f.properties.TNODE_ == currentNode)
|
||
)
|
||
if (segment) {
|
||
fullPath.push(...segment.geometry.coordinates[0])
|
||
}
|
||
}
|
||
}
|
||
|
||
return fullPath
|
||
},
|
||
async calculateShortestPath() {
|
||
if (!this.pointQD || !this.pointZD || !this.roadNetworkGeoJSON) {
|
||
alert('请先加载路网数据并绘制障碍面、起点和终点!')
|
||
return
|
||
}
|
||
this.shortestPathLayer.clear()
|
||
const startPoint = turf.point(
|
||
this.pointQD.toGeoJSON().geometry.coordinates
|
||
).geometry.coordinates // 起点
|
||
const endPoint = turf.point(this.pointZD.toGeoJSON().geometry.coordinates)
|
||
.geometry.coordinates // 终点
|
||
// 途径点
|
||
const viaPointsGeoJSON =
|
||
this.viaPoints.map((point) => point.toGeoJSON()) || []
|
||
const viaPointsTurf = viaPointsGeoJSON.map((p) =>
|
||
turf.point(p.geometry.coordinates)
|
||
)
|
||
// 避让点
|
||
const avoidPointsGeoJSON =
|
||
this.avoidPoints.map((point) => point.toGeoJSON()) || []
|
||
const avoidPointsPolygons = avoidPointsGeoJSON.map((point) => {
|
||
return turf.circle(
|
||
turf.point(point.geometry.coordinates),
|
||
10, // 半径10米(根据需求调整)
|
||
{steps: 32, units: 'meters'} // 显式指定单位
|
||
)
|
||
})
|
||
// 避让区域
|
||
const avoidAreasGeoJSON =
|
||
this.avoidAreas.map((area) => area.toGeoJSON({closure: true})) || []
|
||
const obstaclesGeoJSON = turf.featureCollection([
|
||
...avoidPointsPolygons,
|
||
...avoidAreasGeoJSON,
|
||
])
|
||
const route = await this.planRoute(
|
||
startPoint,
|
||
endPoint,
|
||
viaPointsTurf,
|
||
obstaclesGeoJSON
|
||
)
|
||
this.drawPath(route)
|
||
},
|
||
|
||
// 单独的路径绘制方法
|
||
drawPath(path) {
|
||
const positions = path
|
||
const polyline = new window.mars3d.graphic.PolylinePrimitive({
|
||
positions: positions,
|
||
style: {
|
||
clampToGround: true,
|
||
color: '#55ff33',
|
||
width: 8,
|
||
},
|
||
})
|
||
this.shortestPathLayer.addGraphic(polyline)
|
||
},
|
||
},
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.home-header {
|
||
height: 60px;
|
||
line-height: 60px;
|
||
display: flex;
|
||
align-items: center;
|
||
padding-left: 34px;
|
||
box-sizing: border-box;
|
||
background: #abc6bc;
|
||
}
|
||
.home-header img {
|
||
height: 24px;
|
||
width: 24px;
|
||
margin-right: 12px;
|
||
}
|
||
.sure {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
/* padding: 4px 12px; */
|
||
gap: 10px;
|
||
background: #176363;
|
||
border-radius: 4px;
|
||
width: 54px;
|
||
height: 24px;
|
||
color: #ffffff;
|
||
font-weight: 400;
|
||
font-size: 14px;
|
||
position: absolute;
|
||
right: 30px;
|
||
}
|
||
.home {
|
||
display: flex;
|
||
height: calc(100% - 60px);
|
||
background: #abc6bc;
|
||
}
|
||
.main-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.control-panel {
|
||
width: 340px;
|
||
padding: 20px 26px;
|
||
margin-left: 4px;
|
||
background-size: cover;
|
||
background: #d4e5db;
|
||
}
|
||
|
||
.control-panel .title {
|
||
/* text-align: center; */
|
||
margin-bottom: 10px;
|
||
color: #1c1c1c;
|
||
}
|
||
.importJson {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background: #2f705f;
|
||
border-radius: 4px;
|
||
color: #fff;
|
||
width: 260px;
|
||
height: 24px;
|
||
font-size: 14px;
|
||
text-align: center;
|
||
margin-left: 40px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
padding: 5px;
|
||
border-radius: 3px;
|
||
border: none;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
#map,
|
||
#mapbox,
|
||
.el-main {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
border: 1px solid #333;
|
||
}
|
||
</style> |