Files
kxfx/src/views/home.vue
gcw_IJ7DAiVL a95cd52b80 add
2025-09-10 01:36:34 +08:00

663 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>