This commit is contained in:
gcw_IJ7DAiVL
2025-09-10 01:36:34 +08:00
parent 80f9714845
commit a95cd52b80
10 changed files with 3388 additions and 15 deletions

2481
public/config/dao.geojson Normal file

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,8 @@
<script src="./tools/colormap.js"></script> <script src="./tools/colormap.js"></script>
<script src="./tools/turf.min.js"></script> <script src="./tools/turf.min.js"></script>
<script src="./map/dijkstra.js" type="text/javascript"></script>
<script src="./map/turf/turf.min.js" type="text/javascript"></script>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

165
public/map/dijkstra.js Normal file
View File

@ -0,0 +1,165 @@
'use strict';
/******************************************************************************
* Created 2008-08-19.
*
* Dijkstra path-finding functions. Adapted from the Dijkstar Python project.
*
* Copyright (C) 2008
* Wyatt Baldwin <self@wyattbaldwin.com>
* All rights reserved
*
* Licensed under the MIT license.
*
* http://www.opensource.org/licenses/mit-license.php
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*****************************************************************************/
window.dijkstra = {
single_source_shortest_paths: function(graph, s, d) {
// Predecessor map for each node that has been encountered.
// node ID => predecessor node ID
var predecessors = {};
// Costs of shortest paths from s to all nodes encountered.
// node ID => cost
var costs = {};
costs[s] = 0;
// Costs of shortest paths from s to all nodes encountered; differs from
// `costs` in that it provides easy access to the node that currently has
// the known shortest path from s.
// XXX: Do we actually need both `costs` and `open`?
var open = dijkstra.PriorityQueue.make();
open.push(s, 0);
var closest,
u, v,
cost_of_s_to_u,
adjacent_nodes,
cost_of_e,
cost_of_s_to_u_plus_cost_of_e,
cost_of_s_to_v,
first_visit;
while (!open.empty()) {
// In the nodes remaining in graph that have a known cost from s,
// find the node, u, that currently has the shortest path from s.
closest = open.pop();
u = closest.value;
cost_of_s_to_u = closest.cost;
// Get nodes adjacent to u...
adjacent_nodes = graph[u] || {};
// ...and explore the edges that connect u to those nodes, updating
// the cost of the shortest paths to any or all of those nodes as
// necessary. v is the node across the current edge from u.
for (v in adjacent_nodes) {
if (adjacent_nodes.hasOwnProperty(v)) {
// Get the cost of the edge running from u to v.
cost_of_e = adjacent_nodes[v];
// Cost of s to u plus the cost of u to v across e--this is *a*
// cost from s to v that may or may not be less than the current
// known cost to v.
cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
// If we haven't visited v yet OR if the current known cost from s to
// v is greater than the new cost we just found (cost of s to u plus
// cost of u to v across e), update v's cost in the cost list and
// update v's predecessor in the predecessor list (it's now u).
cost_of_s_to_v = costs[v];
first_visit = (typeof costs[v] === 'undefined');
if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
costs[v] = cost_of_s_to_u_plus_cost_of_e;
open.push(v, cost_of_s_to_u_plus_cost_of_e);
predecessors[v] = u;
}
}
}
}
if (typeof d !== 'undefined' && typeof costs[d] === 'undefined') {
var msg = ['Could not find a path from ', s, ' to ', d, '.'].join('');
throw new Error(msg);
}
return predecessors;
},
extract_shortest_path_from_predecessor_list: function(predecessors, d) {
var nodes = [];
var u = d;
var predecessor;
while (u) {
nodes.push(u);
predecessor = predecessors[u];
u = predecessors[u];
}
nodes.reverse();
return nodes;
},
find_path: function(graph, s, d) {
var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
return dijkstra.extract_shortest_path_from_predecessor_list(
predecessors, d);
},
/**
* A very naive priority queue implementation.
*/
PriorityQueue: {
make: function (opts) {
var T = dijkstra.PriorityQueue,
t = {},
key;
opts = opts || {};
for (key in T) {
if (T.hasOwnProperty(key)) {
t[key] = T[key];
}
}
t.queue = [];
t.sorter = opts.sorter || T.default_sorter;
return t;
},
default_sorter: function (a, b) {
return a.cost - b.cost;
},
/**
* Add a new item to the queue and ensure the highest priority element
* is at the front of the queue.
*/
push: function (value, cost) {
var item = {value: value, cost: cost};
this.queue.push(item);
this.queue.sort(this.sorter);
},
/**
* Return the highest priority element in the queue.
*/
pop: function () {
return this.queue.shift();
},
empty: function () {
return this.queue.length === 0;
}
}
};
// node.js module exports
if (typeof module !== 'undefined') {
module.exports = dijkstra;
}

96
public/map/turf.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
src/assets/image/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/image/avoidP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/image/end.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/image/start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/image/updown.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

View File

@ -1,34 +1,663 @@
<template> <template>
<div id="home"></div> <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> </template>
<script> <script>
import Header from "./header/index.vue"; import Header from './header/index.vue'
export default { export default {
name: "", name: '',
components: { components: {
Header, Header,
}, },
data() { data() {
return {}; 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,
}
}, },
mounted() { async mounted() {
this.receiveBUS(); await this.getMapOption()
this.initMap()
}, },
methods: { methods: {
receiveBUS() { async getMapOption() {
this.$bus.$on("setActiveIndex", (val) => { await fetch('./config/map.json')
console.log("val===>", val); .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> </script>
<style lang="scss" scoped> <style scoped>
#home { .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%; width: 100%;
height: 100%; height: 100%;
} }
</style>
.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>