Files
kxfx/src/views/home.vue
2025-10-10 18:32:13 +08:00

1624 lines
56 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="clear" class="sure">清除</div>
<div @click="calculateShortestPath" class="sure">确定</div>
<div @click="hadBuffer" class="sure">路线隐蔽规划</div>
<div @click="pointBuffer" 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="120px"
label-position="left"
size="mini"
:model="form"
>
<el-form-item label="起点">
<el-input
v-model="form.startPoint"
@blur="pointsChange('startPoint')"
@clear="pointsChange('startPoint')"
clearable
></el-input>
</el-form-item>
<el-form-item label="终点">
<el-input
v-model="form.endPoint"
@blur="pointsChange('endPoint')"
@clear="pointsChange('endPoint')"
clearable
></el-input>
</el-form-item>
<el-form-item label="途经点">
<div v-for="(item, index) in form.viaPoints" :key="index">
<el-input
v-model="item.points"
placeholder=""
@blur="pointsChange('viaPoints', item)"
@clear="pointsChange('viaPoints', item)"
clearable
></el-input>
</div>
</el-form-item>
<el-form-item label="避让点">
<div v-for="(item, index) in form.avoidPoints" :key="index">
<el-input
v-model="item.points"
placeholder=""
@blur="pointsChange('avoidPoints', item)"
@clear="pointsChange('avoidPoints', item)"
clearable
></el-input>
</div>
</el-form-item>
<el-form-item label="避让区域">
<div v-for="(item, index) in form.avoidAreas" :key="index">
<el-input
v-model="item.points"
placeholder=""
@blur="pointsChange('avoidAreas', item)"
@clear="pointsChange('avoidAreas', item)"
clearable
></el-input>
</div>
</el-form-item>
</el-form>
<input type="file" ref="fileInput" @change="handleFileUpload" style="display: none" />
<div class="importJson" @click="triggerFileUpload">导入json文件</div>
</div>
<div class="control-panel">
<div class="title">隐蔽添加</div>
<el-form label-width="120px" label-position="left" size="mini">
<el-form-item label="缓冲半径m">
<el-input v-model="hideform.radius"></el-input>
</el-form-item>
<el-form-item label="面积冗余(%">
<el-input v-model="hideform.redundancy" placeholder=""></el-input>
</el-form-item>
</el-form>
</div>
<div class="control-panel">
<div class="title">
<span>机动属性</span>
<div class="joinCheck">
<el-checkbox v-model="join"></el-checkbox>
<span>参与路线规划</span>
</div>
</div>
<el-form @submit.native.prevent="calculateShortestPath" label-width="120px" label-position="left" size="mini">
<el-form-item label="宽度">
<el-input v-model="inputform.width"></el-input>
</el-form-item>
<el-form-item label="载重(吨)">
<el-input v-model="inputform.load" placeholder=""></el-input>
</el-form-item>
<el-form-item label="最小转弯半径">
<el-input v-model="inputform.minTurnRadius" placeholder=""></el-input>
</el-form-item>
</el-form>
<div class="importJson" @click="openDialog">数据选择</div>
</div>
</div>
<div id="map"></div>
<div class="main-container" style="width: 452px">
<div class="control-panel" style="width: 452px">
<div style="font-size: 14px; margin-bottom: 10px">
详细路线<br />
起点{{ form.startPoint }} 途经点{{
form.viaPoints.length > 0 && form.viaPoints[0].points
? form.viaPoints.map((item) => item.points).join(';')
: ''
}}), 避让点{{
form.avoidPoints.length > 0 && form.avoidPoints[0].points
? form.avoidPoints.map((item) => item.points).join(';')
: ''
}}), 终点{{ form.endPoint }}, 避让区域{{
form.avoidAreas.length > 0 && form.avoidAreas[0].points
? form.avoidAreas.map((item) => item.points).join(';')
: ''
}})<br />
装备参数:最大车辆宽度{{ inputform.width || 0 }},最小转弯半径{{
inputform.minTurnRadius || 0
}}最大车辆载重{{ inputform.load || 0 }}
</div>
<vxe-table ref="xTable" :data="infoList" style="max-height: 50vh; overflow: hidden; overflow-y: auto">
<vxe-column field="编码" title="编码" width="65px"></vxe-column>
<vxe-column field="名称" title="名称" width="80px"></vxe-column>
<vxe-column field="宽度" title="路宽"></vxe-column>
<vxe-column field="曲率半" title="曲率"></vxe-column>
<vxe-column field="载重吨" title="载重"></vxe-column>
<vxe-column field="水深" title="水深"></vxe-column>
<vxe-column field="净空高" title="净空高" width="65px"></vxe-column>
</vxe-table>
<vxe-table :data="factoriesWithVehicles" style="margin-top: 10px">
<vxe-column field="options.style.properties.FID_1" title="厂房id"></vxe-column>
<vxe-column field="area" title="面积(㎡)">
<template v-slot="{row}">
{{ row.area.toFixed(2) }}
</template>
</vxe-column>
<vxe-column field="vehicles" title="车辆">
<template v-slot="{row}">
{{ row.vehicles.map((item) => item.name).join(',') }}
</template>
</vxe-column>
</vxe-table>
</div>
</div>
<el-dialog :visible.sync="dialogVisible" title="车辆选择" width="800px">
<div style="margin-bottom: 10px">
<el-button type="primary" size="mini" @click="handleAdd">新增</el-button>
</div>
<vxe-table
height="300px"
ref="vxeTable"
:data="tableData"
:row-config="{isCurrent: true, isHover: true, keyField: 'id'}"
:checkbox-config="{checkField: 'checked', highlight: true}"
@checkbox-change="handleSelectionChange"
@checkbox-all="handleSelectionChange"
@close="closeSelection"
align="center"
>
<vxe-column type="checkbox" width="50"></vxe-column>
<vxe-column field="name" title="名称" width="100">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.name" size="mini"></el-input>
<span v-else>{{ row.name }}</span>
</template>
</vxe-column>
<vxe-column field="long" title="长度" width="100">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.long" size="mini"></el-input>
<span v-else>{{ row.long }}</span>
</template>
</vxe-column>
<vxe-column field="width" title="宽度" width="100">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.width" size="mini"></el-input>
<span v-else>{{ row.width }}</span>
</template>
</vxe-column>
<vxe-column field="load" title="载重(吨)" width="100">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.load" size="mini"></el-input>
<span v-else>{{ row.load }}</span>
</template>
</vxe-column>
<vxe-column field="minTurnRadius" title="最小转弯半径">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.minTurnRadius" size="mini"></el-input>
<span v-else>{{ row.minTurnRadius }}</span>
</template>
</vxe-column>
<vxe-column title="操作" width="100">
<template v-slot="{row}">
<el-button v-if="!row.editing" type="text" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button v-else type="text" size="mini" @click="handleSave(row)">保存</el-button>
<el-button type="text" size="mini" @click="handleDelete(row)">删除</el-button>
</template>
</vxe-column>
</vxe-table>
<div slot="footer" class="dialog-footer">
<el-button size="mini" @click="closeSelection">取消</el-button>
<el-button type="primary" size="mini" @click="confirmSelection">确定</el-button>
<el-button type="primary" size="mini" @click="suerCofirm">保存</el-button>
</div>
</el-dialog>
</div>
</div>
</template>
<script>
import Cookies from 'js-cookie'
import axios from 'axios'
import iniParser from 'ini-parser'
import configIni from '/public/config.ini'
export default {
data() {
return {
dialogVisible: false,
tableData: [],
multipleSelection: [],
inputform: {
width: 0,
load: 0,
minTurnRadius: 0,
},
form: {
startPoint: '',
endPoint: '',
viaPoints: [{points: '', time: ''}],
avoidPoints: [{points: '', time: ''}],
avoidAreas: [{points: '', time: ''}],
},
viewer: null,
graphicLayer: null,
polygonZAM: null,
pointQD: null,
pointZD: null,
shortestPathLayer: null,
shortestPathList: [],
infoList: [],
roadNetworkLayer: null,
roadNetworkGeoJSON: null,
mapOptions: null,
viaPoints: [], // 途经点
avoidPoints: [], // 避让点
avoidAreas: [], // 避让区域
roadNetworkGeoJSONBuild: null,
hideform: {
radius: 3000,
redundancy: 10,
},
join: false,
bufferLayerList: [], // 缓冲区信息
factoryGeoJSON: [], // 拿到的所有的厂房数据
accordFactoryInfo: [], // 在缓冲区 符合条件的厂房
factoriesWithVehicles: [], // 塞入车的厂房集合
accordFactoryLayer: null, // 隐蔽规划
accordPoint: null, // 隐蔽规划点
}
},
async mounted() {
this.viewer = null
await this.getMapOption()
this.$nextTick(async () => {
await this.initMap()
})
},
beforeDestroy() {
this.destroyMap()
},
methods: {
destroyMap() {
this.clear()
},
async getMapOption() {
await fetch('./config/map.json')
.then((response) => {
return response.json()
})
.then((data) => {
this.mapOptions = data.map3d
})
.catch((error) => {})
},
async initMap() {
const parsedData = iniParser.parse(configIni)
this.viewer = new window.mars3d.Map(
'map',
{
...this.mapOptions,
scene: {
...this.mapOptions.scene,
// mode: Cesium.SceneMode.SCENE2D,
center: {
lat: 27.729862392917948,
lng: 114.27980291774088,
alt: 45000,
heading: 5,
pitch: -35,
},
},
// basemaps: [
// {
// id: "image-tdss",
// name: "影像图",
// type: "xyz",
// // url: `http:/www.tdss.website:280/tiles/img_c/{z}/{x}/{y}`,
// url: `http://${parsedData.http.address}:${parsedData.http.port}/web/imgs/{z}/{x}/{y}`,
// // crs: "EPSG:4490",
// crs: "EPSG:3857",
// show: true
// }
// ],
} || {}
)
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.accordFactoryLayer = new window.mars3d.layer.GraphicLayer()
this.viewer.addLayer(this.accordFactoryLayer)
this.loadShapefile() // 拿到路网数据
// 添加地图点击事件监听,用于结束绘制
this.viewer.on(mars3d.EventType.dblClick, (event) => {
// 如果正在绘制,点击地图可以结束绘制(除了绘制点)
this.graphicLayer.stopDraw()
})
},
clear() {
this.graphicLayer.stopDraw()
// 清除起点
if (this.pointQD) {
this.pointQD.remove()
this.pointQD = null
this.form.startPoint = ''
}
// 清除终点
if (this.pointZD) {
this.pointZD.remove()
this.pointZD = null
this.form.endPoint = ''
}
if (this.viaPoints.length > 0) {
// 清除途经点
this.viaPoints.forEach((point) => {
point.remove()
})
this.viaPoints = []
this.form.viaPoints = [{points: '', time: ''}]
}
if (this.avoidPoints.length > 0) {
// 清除避让点
this.avoidPoints.forEach((point) => {
point.remove()
})
this.avoidPoints = []
this.form.avoidPoints = [{points: '', time: ''}]
}
if (this.avoidAreas.length > 0) {
// 清除避让区域
this.avoidAreas.forEach((area) => {
area.remove()
})
this.avoidAreas = []
this.form.avoidAreas = [{points: '', time: ''}]
}
if (this.shortestPathLayer) {
// 清除最短路径图层
this.shortestPathLayer.clear()
this.infoList = []
this.shortestPathList = []
}
if (this.accordFactoryLayer) {
// 清除路线隐蔽规划
this.accordFactoryLayer.clear()
this.accordFactoryInfo = []
this.bufferLayerList = []
this.factoriesWithVehicles = []
}
},
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/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 graphicLine = new window.mars3d.graphic.PolylineEntity({
positions: feature.geometry.coordinates[0],
style: {
color: '#FF0000',
width: 2,
outline: false,
},
})
this.graphicLayer.addGraphic(graphicLine)
})
this.roadNetworkGeoJSONBuild = this.buildGraph(this.roadNetworkGeoJSON)
this.loadFactoryGeoJson() // 拿到厂房数据
} catch (error) {
console.error('加载 Shapefile 数据失败:', error)
}
},
// 获取厂房的数据
async loadFactoryGeoJson() {
try {
const shpBuffer = await fetch('./config/factory.geojson')
.then((response) => {
return response.json()
})
.then((data) => {
return data
})
.catch((error) => {})
this.factoryGeoJSON = shpBuffer
} catch (error) {
console.error('加载厂房数据失败:', error)
}
},
// 去绘制点 进行隐蔽规划
pointBuffer() {
if (this.shortestPathList.length > 0 || this.accordFactoryInfo > 0) {
this.$message.warning('请先清空路线以及路线隐蔽规划')
return
}
if (this.accordPoint) {
this.accordPoint.remove()
this.accordPoint = null
}
if (this.accordFactoryLayer) {
// 清除点的隐蔽规划
this.accordFactoryLayer.clear()
this.accordFactoryInfo = []
this.factoriesWithVehicles = []
}
this.accordFactoryLayer.startDraw({
type: 'point',
style: {
pixelSize: 10,
color: '#55ff33',
},
success: (graphic) => {
this.accordPoint = graphic
try {
// 创建缓冲区的宽度
const bufferWidth = this.hideform.radius // 避让区的宽度(单位:米)
// 获取绘制的点的坐标
const pointCoordinates = [
graphic.toGeoJSON().geometry.coordinates[0],
graphic.toGeoJSON().geometry.coordinates[1],
]
// 创建点的 GeoJSON 特征
const pointFeature = {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: pointCoordinates,
},
properties: {},
}
// 使用 Turf.js 计算缓冲区
let buffered = turf.buffer(pointFeature, bufferWidth / 1000, {units: 'kilometers'})
// 将缓冲区存储到数组中
this.bufferLayerList.push(buffered)
const polygon = new window.mars3d.graphic.PolygonEntity({
positions: buffered.geometry.coordinates[0],
style: {
color: '#d4e5db',
opacity: 0.5,
outline: true,
outlineWidth: 1,
outlineColor: '#ffffff',
},
time: new Date().getTime(),
})
this.accordFactoryLayer.addGraphic(polygon)
// 检查缓冲区内的厂房
this.checkFactoryInBuffer('point', pointCoordinates)
} catch (error) {
console.error('缓冲区生成或检查异常:', error)
}
},
})
},
// 获取当前路线的缓冲区
hadBuffer() {
if (this.shortestPathList.length == 0) {
this.$message.warning('请先进行路线规划')
return
}
if (JSON.parse(Cookies.get('minTurnRadius')).length == 0) {
this.$message.warning('请先选择车辆')
return
}
try {
// =======检查几何对象的类型 缓冲区========
for (const feature of this.shortestPathList[0]) {
// this.shortestPathList[0].forEach((feature) => {
// 创建缓冲区的宽度
const bufferWidth = this.hideform.radius // 避让区的宽度(单位:米)
if (feature.geometry.type === 'LineString') {
const positions = feature.geometry.coordinates[0]
// 确保每条线至少有 2 个点
if (positions.length < 2) {
console.warn('无效的线,跳过:', feature)
return
}
try {
// 使用 Turf.js 计算缓冲区
var buffered = turf.buffer(feature, bufferWidth / 1000, {units: 'kilometers'})
// 将缓冲区存储到数组中
this.bufferLayerList.push(buffered)
} catch (error) {
console.error('缓冲分析异常:', error)
}
} else if (feature.geometry.type === 'MultiLineString') {
// 遍历每个线段
feature.geometry.coordinates.forEach((lineCoordinates) => {
// 检查每个线段是否有效
if (lineCoordinates.length < 2) {
console.warn('多线段中的无效的线段,跳过:', lineCoordinates)
return
}
// 创建单个线段的 GeoJSON 特征
const lineFeature = {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: lineCoordinates,
},
properties: feature.properties,
}
try {
// 使用 Turf.js 计算缓冲区
var buffered = turf.buffer(lineFeature, bufferWidth / 1000, {units: 'kilometers'})
// 将缓冲区存储到数组中
this.bufferLayerList.push(buffered)
} catch (error) {
console.error('缓冲分析异常:', error)
}
})
} else {
console.warn('不支持的几何类型:', feature.geometry.type)
}
}
// 去筛选在缓存区的厂房
this.checkFactoryInBuffer('line')
} catch (error) {
console.error('处理路径列表时发生错误:', error)
}
},
checkFactoryInBuffer(type, pointInfo) {
if (!this.factoryGeoJSON || !this.bufferLayerList.length) {
this.$message.warning('厂房数据或缓冲区数据未加载')
return
}
if (JSON.parse(Cookies.get('minTurnRadius')).length == 0) {
this.$message.warning('请先选择车辆')
return
}
// 将缓冲区数据转换为 Turf.js 的 FeatureCollection
// 遍历每个厂房
const factoryGeoJSON = JSON.parse(JSON.stringify(this.factoryGeoJSON))
const factoryPromises = factoryGeoJSON.features.map((factory) => {
return new Promise((resolve, reject) => {
// 检查几何对象的类型
if (factory.geometry.type === 'MultiPolygon') {
// 创建多边形集合MultiPolygon
const factoryMultiPoly = turf.multiPolygon(factory.geometry.coordinates)
// 计算整个多边形集合的面积
const area = turf.area(factoryMultiPoly)
// 计算工厂多边形集合的边界框
const [minX, minY, maxX, maxY] = turf.bbox(factoryMultiPoly)
// 筛选与工厂边界框相交的缓冲区
const candidates = this.bufferLayerList.filter((buffer) => {
const [bminX, bminY, bmaxX, bmaxY] = turf.bbox(buffer)
return !(bmaxX < minX || bminX > maxX || bmaxY < minY || bminY > maxY)
})
// 精确相交检测
let isInside = false
for (const buffer of candidates) {
if (turf.booleanIntersects(factoryMultiPoly, buffer)) {
// 如果工厂在缓冲区内,绘制所有多边形
isInside = true
this.drawFactory(factory.geometry.coordinates, factory, area, pointInfo)
break
}
}
// 如果工厂不在任何缓冲区内
resolve()
} else {
reject(new Error('不支持的几何类型'))
}
})
})
// 确保所有工厂处理完成后进行排序
Promise.allSettled(factoryPromises)
.then(async (results) => {
// 只有路线隐蔽规划需要塞入车辆 点的不需要
// 所有工厂处理完成,进行排序 按照距离近到远
this.accordFactoryInfo.sort((a, b) => a.distance - b.distance)
// 拿到当前的车队车辆信息 并且算出面积 按照从大到小排序
const areaList = JSON.parse(Cookies.get('minTurnRadius'))
areaList.map((item) => {
item.area = Number(item.long) * Number(item.width)
})
areaList.sort((a, b) => b.area - a.area)
// 初始化每个工厂的剩余面积
this.accordFactoryInfo.forEach((factory) => {
factory.remainingArea = factory.area // 记录每个工厂的剩余可用面积
factory.vehicles = [] // 确保每个工厂都有一个 vehicles 属性
})
// 遍历每个车辆,尝试塞入工厂
areaList.forEach((vehicle) => {
let isPlaced = false
// 遍历每个工厂,尝试塞入车辆
for (let i = 0; i < this.accordFactoryInfo.length; i++) {
const factory = this.accordFactoryInfo[i]
// 检查工厂是否还有剩余面积
if (factory.remainingArea >= vehicle.area) {
// 塞入车辆
factory.vehicles.push({
...vehicle,
remainingArea: factory.remainingArea - vehicle.area, // 记录车辆塞入后的工厂剩余面积
})
// 更新工厂的剩余面积
factory.remainingArea -= vehicle.area
// 标记车辆已放置
isPlaced = true
break
}
}
// 如果车辆没有被放置,可以在这里处理(例如记录日志或显示警告)
if (!isPlaced) {
console.warn(`车辆 ${vehicle.id} 无法放置在任何工厂中`)
}
})
// 更新 this.accordFactoryInfo
this.accordFactoryInfo = this.accordFactoryInfo.map((factory) => {
// 确保每个工厂都有 vehicles 属性
if (!factory.vehicles) {
factory.vehicles = []
}
// 确保 remainingArea 是一个数字且大于 0
factory.area = factory.area > 0 ? factory.area : 0
return factory
})
// 过滤出有车辆的工厂
const factoriesWithVehicles = this.accordFactoryInfo.filter((factory) => factory.vehicles.length > 0)
this.factoriesWithVehicles = factoriesWithVehicles
await this.showAreaInfoDialog(factoriesWithVehicles)
})
.catch((error) => {
console.error('数据有问题', error)
})
},
drawFactory(factoryMultiPoly, factory, area, pointInfo) {
// 获取当前多边形集合的中心点
const center = turf.center(turf.multiPolygon(factory.geometry.coordinates))
// 将中心点转换为 Mars3D 的 LngLatPoint 对象
const popupPosition = new mars3d.LngLatPoint(center.geometry.coordinates[0], center.geometry.coordinates[1])
// 遍历每个多边形集合
factory.geometry.coordinates[0].forEach((coordGroup) => {
// 创建一个多边形图形
const graphic = new window.mars3d.graphic.PolygonEntity({
positions: coordGroup.map((coord) => Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 0)),
style: {
color: 'red',
opacity: 0.4,
outline: true,
outlineWidth: 1,
outlineColor: '#ffffff',
properties: {...factory.properties},
},
time: new Date().getTime(),
})
this.accordFactoryLayer.addGraphic(graphic)
// 计算多边形中心与参考点的距离
const point = pointInfo && pointInfo.length > 0 ? pointInfo : this.form.endPoint.split(',').map(Number)
const distance = turf.distance(center.geometry.coordinates, point, {units: 'kilometers'}) * 1000 // 米
// 将多边形图形添加到地图层
this.accordFactoryInfo.push({
...graphic,
distance: distance,
popupPosition: popupPosition,
area:
this.hideform.redundancy && this.hideform.redundancy != 0 && area > 0
? area * (100 - this.hideform.redundancy) * 0.01
: area > 0
? area
: 0, // 如果 area 不是有效值,设置为 0 或其他默认值
})
})
},
// 显示面积信息弹框的方法
async showAreaInfoDialog(info) {
const graphics = this.accordFactoryLayer.getGraphics()
const promises = []
info.forEach((item) => {
graphics.forEach((ele) => {
if (
ele.options.time === item.options.time &&
ele.options.style.properties.FID_1 === item.options.style.properties.FID_1
) {
const textInfo = `${item.area.toFixed(2)}\n${item.vehicles.map((e) => e.name).join(',')}`
const labelGraphic = new window.mars3d.graphic.LabelEntity({
id: 'label',
position: item.popupPosition,
style: {
text: String(textInfo), // 必须字符串
font_family: '楷体',
font_size: 24,
color: '#ffffff',
outline: true,
outlineColor: '#000000',
background: true,
backgroundColor: '#2f705f',
},
})
// 使用 Promise 确保标签添加完成后继续执行
const promise = new Promise((resolve, reject) => {
this.accordFactoryLayer.addGraphic(labelGraphic, () => {
resolve()
})
})
promises.push(promise)
}
})
})
this.$message.success('路线隐蔽规划成功')
const popupPositionList = info.map((item) => item.popupPosition)
let minLng = Infinity
let maxLng = -Infinity
let minLat = Infinity
let maxLat = -Infinity
popupPositionList.forEach((point) => {
if (point.lng < minLng) minLng = point.lng
if (point.lng > maxLng) maxLng = point.lng
if (point.lat < minLat) minLat = point.lat
if (point.lat > maxLat) maxLat = point.lat
})
// 创建一个矩形区域,包含所有点
const rectangle = Cesium.Rectangle.fromDegrees(minLng, minLat, maxLng, maxLat)
this.viewer.scene.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(
(minLng + maxLng) / 2, // 中心经度
(minLat + maxLat) / 2, // 中心纬度
1000 // 增加高度,确保视野范围足够大
),
orientation: {
heading: Cesium.Math.toRadians(0), // 方位角
pitch: Cesium.Math.toRadians(-90), // 俯仰角
roll: 0.0, // 翻滚角
},
duration: 2, // 飞行动画持续时间,单位为秒
})
},
// 弹框
async openDialog() {
this.multipleSelection = []
await fetch('./data/minTurnRadius.json')
.then((response) => {
return response.json()
})
.then(async (data) => {
this.dialogVisible = true
this.tableData = data
await this.$nextTick(() => {
this.multipleSelection = JSON.parse(Cookies.get('minTurnRadius'))
this.multipleSelection.forEach((item) => {
this.$refs.vxeTable.setCheckboxRowKey(item.id, true)
})
})
})
.catch((error) => {})
},
handleSelectionChange({records}) {
this.multipleSelection = records
},
closeSelection() {
this.dialogVisible = false
},
confirmSelection() {
if (this.multipleSelection.length === 0) {
this.$message.warning('请至少选择一行数据')
return
}
Cookies.set('minTurnRadius', this.multipleSelection)
const maxValues = this.multipleSelection.reduce(
(acc, item) => {
acc.width = Math.max(acc.width, item.width)
acc.load = Math.max(acc.load, item.load)
acc.minTurnRadius = Math.max(acc.minTurnRadius, item.minTurnRadius)
return acc
},
{width: 0, load: 0, minTurnRadius: 0}
)
this.inputform.width = maxValues.width
this.inputform.load = maxValues.load
this.inputform.minTurnRadius = maxValues.minTurnRadius
this.closeSelection()
},
/** 增删改查 */
handleAdd() {
const newRow = {
id: this.tableData.length + 1,
name: '',
long: null,
width: null,
load: null,
minTurnRadius: null,
editing: true,
}
this.tableData.push(newRow)
},
handleDelete(row) {
const index = this.tableData.findIndex((item) => item.id === row.id)
if (index !== -1) {
this.tableData.splice(index, 1)
}
},
handleEdit(row) {
this.$set(row, 'editing', true)
},
handleSave(row) {
this.$set(row, 'editing', false)
},
// 保存列表数据
suerCofirm() {
const parsedData = iniParser.parse(configIni)
axios
.post(
`http://${parsedData.http.address}:${parsedData.http.port}/api/equpment`,
JSON.stringify(this.tableData),
{
headers: {
Authorization: 'Bearer your_token_here',
'Content-Type': 'application/json',
},
}
)
.then((response) => {
this.$message.success('保存成功')
})
.catch((error) => {})
},
// 导入json文件
triggerFileUpload() {
this.$refs.fileInput.click()
},
handleFileUpload(event) {
const file = event.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
try {
const uploadedJson = JSON.parse(e.target.result)
this.importJson(uploadedJson)
} catch (error) {
console.error('解析 JSON 文件失败:', error)
this.$message.error('解析 JSON 文件失败')
}
}
reader.readAsText(file)
}
},
async importJson(uploadedJson) {
if (!uploadedJson) {
this.$message.warning('请先选择一个 JSON 文件')
return
}
// 清除现有数据
this.clear()
// 加载起点
if (uploadedJson.startPoint) {
this.form.startPoint = uploadedJson.startPoint
this.addPointToMap('startPoint', this.form.startPoint)
}
// 加载终点
if (uploadedJson.endPoint) {
this.form.endPoint = uploadedJson.endPoint
this.addPointToMap('endPoint', this.form.endPoint)
}
// 加载途经点
if (uploadedJson.viaPoints) {
this.form.viaPoints = uploadedJson.viaPoints
uploadedJson.viaPoints.forEach((viaPoint) => {
this.addPointToMap('viaPoints', viaPoint, viaPoint.time)
})
}
// 加载避让点
if (uploadedJson.avoidPoints) {
this.form.avoidPoints = uploadedJson.avoidPoints
uploadedJson.avoidPoints.forEach((avoidPoint) => {
this.addPointToMap('avoidPoints', avoidPoint, avoidPoint.time)
})
}
// 加载避让区域
if (uploadedJson.avoidAreas) {
this.form.avoidAreas = uploadedJson.avoidAreas
uploadedJson.avoidAreas.forEach((avoidArea) => {
const time = Date.now()
this.addPolygonToMap('avoidAreas', avoidArea, avoidArea.time)
})
}
},
addPointToMap(type, point, time) {
const coords = time ? point.points.split(',').map(Number) : point.split(',').map(Number)
const graphic = new window.mars3d.graphic.PointEntity({
position: new window.mars3d.LngLatPoint(coords[0], coords[1]),
style: {
pixelSize: 10,
color: type === 'startPoint' ? 'red' : type === 'endPoint' ? 'red' : type === 'viaPoints' ? 'blue' : 'orange',
label: {
text:
type === 'startPoint'
? '起点'
: type === 'endPoint'
? '终点'
: type === 'viaPoints'
? '途经点'
: '避让点',
font_size: 20,
color: '#ffffff',
outline: true,
outlineColor: '#000000',
pixelOffsetY: -20,
},
time: time,
},
})
this.graphicLayer.addGraphic(graphic)
if (type === 'startPoint') {
this.pointQD = graphic
} else if (type === 'endPoint') {
this.pointZD = graphic
} else if (type === 'viaPoints') {
this.viaPoints.push(graphic)
} else if (type === 'avoidPoints') {
this.avoidPoints.push(graphic)
}
},
addPolygonToMap(type, area, time) {
const coords = JSON.parse(area.points)
const graphic = new window.mars3d.graphic.PolygonEntity({
positions: coords.map((coord) => new window.mars3d.LngLatPoint(coord[0], coord[1])),
style: {
color: 'red',
opacity: 0.4,
clampToGround: true,
outline: true,
outlineWidth: 1,
outlineColor: '#ffffff',
time: time,
},
})
this.graphicLayer.addGraphic(graphic)
if (type === 'avoidAreas') {
this.avoidAreas.push(graphic)
}
},
// 输入框失去焦点 反向编辑点
pointsChange(type, row) {
if (type === 'startPoint') {
if (
(!this.form.startPoint ||
this.form.startPoint == '' ||
this.form.startPoint == null ||
this.form.startPoint == undefined) &&
this.pointQD
) {
this.pointQD.remove()
this.pointQD = null
return
}
if (this.pointQD == null || this.pointQD == undefined || this.pointQD == '') {
this.addPointToMap('startPoint', this.form.startPoint)
} else {
this.updatePointPosition(this.pointQD, this.form.startPoint)
}
} else if (type === 'endPoint') {
if (
(!this.form.endPoint ||
this.form.endPoint == '' ||
this.form.endPoint == null ||
this.form.endPoint == undefined) &&
this.pointZD
) {
this.pointZD.remove()
this.pointZD = null
return
}
if (this.pointZD == null || this.pointZD == undefined || this.pointZD == '') {
this.addPointToMap('endPoint', this.form.endPoint)
} else {
this.updatePointPosition(this.pointZD, this.form.endPoint)
}
} else if (type === 'viaPoints') {
if (!row.points) {
const graphic = this.viaPoints.find((viaPoint) => viaPoint.style.time === row.time)
if (this.form.viaPoints.length === 1 && this.form.viaPoints[0].points === '') {
// 如果只剩下一个空项,不删除图形,清空输入框值
this.form.viaPoints[0].points = ''
graphic?.remove()
this.viaPoints = this.viaPoints.filter((viaPoint) => viaPoint.style.time !== row.time)
} else {
this.form.viaPoints = this.form.viaPoints.filter((viaPoint) => viaPoint.time !== row.time)
}
graphic?.remove()
this.viaPoints = this.viaPoints.filter((viaPoint) => viaPoint.style.time !== row.time)
} else {
const graphic = this.viaPoints.find((viaPoint) => viaPoint.style.time === row.time)
this.updatePointPosition(graphic, row.points)
}
} else if (type === 'avoidPoints') {
if (!row.points) {
const graphic = this.avoidPoints.find((avoidPoint) => avoidPoint.style.time === row.time)
if (this.form.avoidPoints.length === 1 && this.form.avoidPoints[0].points === '') {
// 如果只剩下一个空项,不删除图形,清空输入框值
this.form.avoidPoints[0].points = ''
} else {
this.form.avoidPoints = this.form.avoidPoints.filter((avoidPoint) => avoidPoint.time !== row.time)
}
graphic?.remove()
this.avoidPoints = this.avoidPoints.filter((avoidPoint) => avoidPoint.style.time !== row.time)
} else {
const graphic = this.avoidPoints.find((avoidPoint) => avoidPoint.style.time === row.time)
this.updatePointPosition(graphic, row.points)
}
} else if (type === 'avoidAreas') {
if (!row.points) {
const graphic = this.avoidAreas.find((avoidArea) => avoidArea.style.time === row.time)
if (this.form.avoidAreas.length === 1 && this.form.avoidAreas[0].points === '') {
// 如果只剩下一个空项,不删除图形,清空输入框值
this.form.avoidAreas[0].points = ''
} else {
this.form.avoidAreas = this.form.avoidAreas.filter((avoidArea) => avoidArea.time !== row.time)
}
graphic?.remove()
this.avoidAreas = this.avoidAreas.filter((avoidArea) => avoidArea.style.time !== row.time)
} else {
const graphic = this.avoidAreas.find((avoidArea) => avoidArea.style.time === row.time)
this.updatePolygonPosition(graphic, row.points)
}
}
},
handleEmptyArray(type) {
if (type === 'viaPoints' && this.form.viaPoints.length === 1 && this.form.viaPoints[0].points === '') {
this.form.viaPoints = []
} else if (
type === 'avoidPoints' &&
this.form.avoidPoints.length === 1 &&
this.form.avoidPoints[0].points === ''
) {
this.form.avoidPoints = []
} else if (type === 'avoidAreas' && this.form.avoidAreas.length === 1 && this.form.avoidAreas[0].points === '') {
this.form.avoidAreas = []
}
},
updatePointPosition(graphic, pointsStr) {
if (!graphic) return
const coords = pointsStr.split(',').map(Number)
if (coords.length === 2) {
graphic.position = new window.mars3d.LngLatPoint(coords[0], coords[1])
}
},
updatePolygonPosition(graphic, pointsStr) {
if (!graphic) return
try {
const coords = JSON.parse(pointsStr)
graphic.positions = coords.map((coord) => new window.mars3d.LngLatPoint(coord[0], coord[1]))
} catch (error) {
graphic.remove()
}
},
drawStartPoint() {
if (this.pointQD) {
this.pointQD.remove()
this.pointQD = null
this.form.startPoint = ''
}
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.form.startPoint = [
graphic.toGeoJSON().geometry.coordinates[0],
graphic.toGeoJSON().geometry.coordinates[1],
].join(',')
},
})
},
drawEndPoint() {
if (this.pointZD) {
this.pointZD.remove()
this.pointZD = null
this.form.endPoint = ''
}
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.form.endPoint = [
graphic.toGeoJSON().geometry.coordinates[0],
graphic.toGeoJSON().geometry.coordinates[1],
].join(',')
},
})
},
// 途经点
drawViaPoint() {
const time = Date.now()
this.graphicLayer.startDraw({
type: 'point',
style: {
pixelSize: 10,
color: 'blue',
label: {
text: '途经点',
font_size: 20,
color: '#ffffff',
outline: true,
outlineColor: '#000000',
pixelOffsetY: -20,
},
time: time,
},
success: (graphic) => {
this.viaPoints.push(graphic)
const points = [
graphic.toGeoJSON().geometry.coordinates[0],
graphic.toGeoJSON().geometry.coordinates[1],
].join(',')
if (
this.form.viaPoints.length == 1 &&
this.form.viaPoints[0].points == '' &&
!this.form.viaPoints[0].points
) {
this.form.viaPoints[0].points = points
this.form.viaPoints[0].time = time
} else {
this.form.viaPoints.push({points, time})
}
},
})
},
// 避让点
drawAvoidPoint() {
const time = Date.now()
this.graphicLayer.startDraw({
type: 'point',
style: {
pixelSize: 10,
color: 'orange',
label: {
text: '避让点',
font_size: 20,
color: '#ffffff',
outline: true,
outlineColor: '#000000',
pixelOffsetY: -20,
},
time: time,
},
success: (graphic) => {
this.avoidPoints.push(graphic)
const points = [
graphic.toGeoJSON().geometry.coordinates[0],
graphic.toGeoJSON().geometry.coordinates[1],
].join(',')
if (
this.form.avoidPoints.length == 1 &&
this.form.avoidPoints[0].points == '' &&
!this.form.avoidPoints[0].points
) {
this.form.avoidPoints[0].points = points
this.form.avoidPoints[0].time = time
} else {
this.form.avoidPoints.push({points, time})
}
},
})
},
// 避让区域
drawAvoidArea() {
const time = Date.now()
this.graphicLayer.startDraw({
type: 'polygon',
drawEndEventType: window.mars3d.EventType.dblClick,
style: {
color: '#ff0000',
opacity: 0.4,
clampToGround: true,
outline: true,
outlineWidth: 1,
outlineColor: '#ffffff',
time: time,
},
success: (graphic) => {
this.avoidAreas.push(graphic)
const avoidAreasGeoJSON = graphic.toGeoJSON()
const points = JSON.stringify(avoidAreasGeoJSON.geometry.coordinates[0])
if (
this.form.avoidAreas.length == 1 &&
this.form.avoidAreas[0].points == '' &&
!this.form.avoidAreas[0].points
) {
this.form.avoidAreas[0].points = points
this.form.avoidAreas[0].time = time
} else {
this.form.avoidAreas.push({points, time})
}
},
})
},
// 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 = [startCoord]
if (viaPoints && viaPoints.length > 0) {
viaPoints.forEach((viaPoint) => {
points.push(viaPoint.geometry.coordinates)
})
}
points.push(endCoord)
const fullPath = []
const infoList = []
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]
let segment = {}
// 是否参与路线规划 是 加入机动属性的判断 否就正常走
if (!this.join) {
segment = this.roadNetworkGeoJSON.features.find(
(f) =>
(f.properties.FNODE_ == currentNode && f.properties.TNODE_ == nextNode) ||
(f.properties.FNODE_ == nextNode && f.properties.TNODE_ == currentNode)
)
} else {
segment = this.roadNetworkGeoJSON.features.find(
(f) =>
((f.properties.FNODE_ == currentNode && f.properties.TNODE_ == nextNode) ||
(f.properties.FNODE_ == nextNode && f.properties.TNODE_ == currentNode)) &&
f.properties.载重吨 >= this.inputform.load &&
f.properties.宽度 >= this.inputform.width &&
f.properties.曲率半 <= this.inputform.minTurnRadius
)
}
if (segment) {
fullPath.push(...segment.geometry.coordinates[0])
infoList.push(segment)
}
}
}
return {fullPath, infoList}
},
async calculateShortestPath() {
if (!this.pointQD || !this.pointZD || !this.roadNetworkGeoJSON) {
this.$message.warning('请先加载路网数据并绘制障碍面、起点和终点!')
return
}
this.shortestPathLayer.clear()
this.shortestPathList = []
this.infoList = []
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),
1000, // 半径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
if (positions.fullPath.length == 0) return
const polyline = new window.mars3d.graphic.PolylinePrimitive({
positions: positions.fullPath,
style: {
clampToGround: true,
color: '#55ff33',
width: 8,
},
})
this.shortestPathLayer.addGraphic(polyline)
this.shortestPathList.push(path.infoList)
this.infoList = path.infoList.map((item) => item.properties)
},
/** 隐蔽规划 */
concealed() {},
},
}
</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;
cursor: pointer;
}
.sure {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
/* padding: 4px 12px; */
gap: 10px;
background: #176363;
border-radius: 4px;
min-width: 14px;
padding: 0 10px;
height: 24px;
color: #ffffff;
font-weight: 400;
font-size: 14px;
margin-right: 10px;
cursor: pointer;
}
.dialog-body {
max-height: 500px;
}
.home {
display: flex;
height: calc(100% - 60px);
background: #abc6bc;
}
.main-container {
width: 340px;
height: 100%;
background: #d4e5db;
overflow: hidden;
overflow-y: auto;
scrollbar-width: none; /* 隐藏滚动条Firefox */
}
.main-container::-webkit-scrollbar {
display: none; /* 隐藏滚动条Chrome, Safari, Edge */
}
.control-panel {
width: 340px;
padding: 20px 26px;
background-size: cover;
box-sizing: border-box;
}
.control-panel .title {
/* text-align: center; */
margin-bottom: 10px;
color: #1c1c1c;
display: flex;
justify-content: space-between;
}
.control-panel .title .joinCheck {
font-size: 14px;
color: #555;
}
.el-checkbox {
margin-right: 5px !important;
}
.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: 20px;
cursor: pointer;
}
.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: calc(100vw- 792px);
height: 100%;
}
.popDiloag {
width: 100px;
font-size: 16px;
background: #d4e5db;
color: #1c1c1c;
}
.popDiloag .popDiloag-title {
font-weight: 500;
}
</style>