1624 lines
56 KiB
Vue
1624 lines
56 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="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>
|