diff --git a/README.md b/README.md index 7540bc4..a19a4ec 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,3 @@ # kxfx -## Project setup - -``` -npm install -``` - -### Compiles and hot-reloads for development - -``` -npm run serve -``` - -### Compiles and minifies for production - -``` -npm run build -``` - -### Lints and fixes files - -``` -npm run lint -``` - -### Customize configuration - -See [Configuration Reference](https://cli.vuejs.org/config/). +## 机动路线规划 diff --git a/public/config.ini b/public/config.ini index 4bcabac..b55b302 100644 --- a/public/config.ini +++ b/public/config.ini @@ -1,5 +1,5 @@ [http] -port=8081 +port=8083 address=127.0.0.1 [title] diff --git a/src/views/home/home.vue b/src/views/home/home.vue index db4748c..9e79994 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -1,15 +1,17 @@ @@ -231,11 +368,11 @@ import Cookies from 'js-cookie' import axios from 'axios' import iniParser from 'ini-parser' -import configIni from '/public/config.ini' export default { data() { return { + mapLoading: false, dialogVisible: false, tableData: [], multipleSelection: [], @@ -277,6 +414,15 @@ export default { factoriesWithVehicles: [], // 塞入车的厂房集合 accordFactoryLayer: null, // 隐蔽规划 accordPoint: null, // 隐蔽规划点 + dialogExportVisible: false, + routeData: [], + hideData: [], + dialogJsonVisible: false, + jsonInfo: { + json: '', + path: '', + }, + jsonLoading: false, } }, async mounted() { @@ -304,7 +450,7 @@ export default { .catch((error) => {}) }, async initMap() { - const parsedData = iniParser.parse(configIni) + this.mapLoading = true this.viewer = new window.mars3d.Map( 'map', { @@ -423,19 +569,56 @@ export default { // 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); + // }) + // 定义颜色数组 + const colors = [ + '#FF0000', + '#00FF00', + '#0000FF', + '#FFFF00', + '#FF00FF', + '#00FFFF', + '#FFA500', + '#800080', + '#008000', + '#000080', + ] + + // 绘制路网线时循环使用颜色 + let colorIndex = 0 // 用于跟踪当前颜色索引 this.roadNetworkGeoJSON.features.forEach((feature) => { - // ====网路图===== + // 获取当前颜色 + const currentColor = colors[colorIndex % colors.length] + + // 创建路网线图形 const graphicLine = new window.mars3d.graphic.PolylineEntity({ positions: feature.geometry.coordinates[0], style: { - color: '#FF0000', + color: '#FF0000', // 使用当前颜色 width: 2, outline: false, }, }) + + // 将图形添加到图层 this.graphicLayer.addGraphic(graphicLine) + + // 更新颜色索引 + colorIndex++ }) this.roadNetworkGeoJSONBuild = this.buildGraph(this.roadNetworkGeoJSON) + this.mapLoading = false this.loadFactoryGeoJson() // 拿到厂房数据 } catch (error) { console.error('加载 Shapefile 数据失败:', error) @@ -890,22 +1073,25 @@ export default { }, // 保存列表数据 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('保存成功') + fetch('./config.ini') + .then((response) => response.text()) + .then((text) => { + const parsedData = iniParser.parse(text) + axios + .post( + `http://${parsedData.http.address}:${parsedData.http.port}/api/equpment`, + JSON.stringify(this.tableData), + { + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .then((response) => { + this.$message.success('保存成功') + }) + .catch((error) => {}) }) - .catch((error) => {}) }, // 导入json文件 triggerFileUpload() { @@ -1391,510 +1577,430 @@ export default { const fullPath = [] const infoList = [] - let prevSegmentEndNode = null // 存储上一个途经点的衔接点(用于下一段复用) - for (let i = 0; i < points.length - 1; i++) { const currPoint = points[i] // 当前点(起点/途经点) const nextPoint = points[i + 1] // 下一个点(途经点/终点) const segmentStart = currPoint.coord const segmentEnd = nextPoint.coord - // 匹配最近节点:关键修改——若当前是“途经点之后的段”,复用前一段的衔接点作为起点 - let startNode, endNode - if (currPoint.type === 'via' && prevSegmentEndNode) { - // 直接使用上一段的结束节点作为起点(确保路径连续性) - startNode = prevSegmentEndNode - } else { - // 情况2:起点/第一段,正常匹配最近节点 - startNode = this.findNearestNode(segmentStart, nodeCoords) - } - endNode = this.findNearestNodeWithReturn(segmentEnd, nodeCoords, tempGraph, startNode) + // 查找最近的边(线段)而不是节点 + let startEdgeInfo = this.findNearestEdge(segmentStart, tempGraph, nodeCoords) + let endEdgeInfo = this.findNearestEdge(segmentEnd, tempGraph, nodeCoords) - if (!startNode || !endNode) { - console.error('无法匹配到路网节点') - prevSegmentEndNode = null // 重置衔接点,避免后续复用错误 + if (!startEdgeInfo || !endEdgeInfo) { + console.error('无法匹配到路网边') continue } + // 计算垂直连接点 + const startConnection = this.calculatePerpendicularConnection(segmentStart, startEdgeInfo, currPoint.type) + const endConnection = this.calculatePerpendicularConnection(segmentEnd, endEdgeInfo, nextPoint.type) - // 检查路径可行性 - if (!this.isPathPossible(tempGraph, startNode, endNode)) { - this.$message.warning(`无法匹配到路网节点`) - continue - } + // 构建包含垂直连接点的临时图 + const extendedGraph = this.buildExtendedGraph(tempGraph, nodeCoords, startConnection, endConnection) - // 检查路径可行性(考虑死胡同原路返回) - const {pathNodes, isDeadEnd} = await this.findPathWithReturn(tempGraph, startNode, endNode, nodeCoords) + // 查找路径 + const pathNodes = await this.findPathWithPerpendicularConnections( + extendedGraph, + startConnection.tempNodeId, + endConnection.tempNodeId + ) if (!pathNodes || pathNodes.length === 0) { this.$message.warning(`第${i + 1}段路径未找到!`) - prevSegmentEndNode = null continue } // 生成当前段的路径(处理死胡同原路返回) - const segmentResult = await this.generatePathWithReturn( + const segmentResult = await this.generatePathWithPerpendicularConnections( pathNodes, segmentStart, segmentEnd, - i, - points.length, - currPoint.type, - nextPoint.type, - isDeadEnd, - prevSegmentEndNode + startConnection, + endConnection, + extendedGraph ) if (segmentResult.path.length > 0) { // 合并路径段 this.mergePathSegments(fullPath, segmentResult.path) infoList.push(...segmentResult.segments) - // 记录当前段的结束节点,供下一段使用 - prevSegmentEndNode = pathNodes[pathNodes.length - 1] - // if (i === 0) { - // // 第一段:直接添加 - // fullPath.push(...segmentResult.path); - // } else { - // // 后续段:需要检查连接点,避免重复 - // this.mergePathSegments(fullPath, segmentResult.path); - // } - - // infoList.push(...segmentResult.segments); - // 关键:若当前段的终点是“途经点”,记录其衔接点,供下一段复用 - // if (nextPoint.type === 'via' && segmentResult.viaConnectPoint) { - // prevSegmentEndNode = segmentResult.viaConnectPoint; - // } else { - // prevSegmentEndNode = null; // 非途经点,重置 - // } - } else { - prevSegmentEndNode = null } } return {fullPath, infoList} }, - // 改进:查找最近节点,考虑死胡同情况 - findNearestNodeWithReturn(coord, nodeCoords, graph, fromNode = null) { - const nearestNode = this.findNearestNode(coord, nodeCoords) - - // 如果没有起点节点或不是死胡同,直接返回最近节点 - if (!fromNode || !this.isDeadEnd(nearestNode, graph)) { - return nearestNode + // 合并路径段 + mergePathSegments(fullPath, newSegment) { + if (!newSegment || newSegment.length === 0) return + if (fullPath.length === 0) { + fullPath.push(...newSegment) + return } - - // 如果是死胡同,找到死胡同的入口节点(原路返回点) - return this.findDeadEndEntry(nearestNode, graph, fromNode, nodeCoords) + const lastPoint = fullPath[fullPath.length - 1] + const firstNewPoint = newSegment[0] + const dist = this.calculateDistance(lastPoint, firstNewPoint) + if (dist > 1e-2) { + // 如果断开了超过1cm,就补上连接线 + fullPath.push(firstNewPoint) + } + fullPath.push(...newSegment.slice(1)) }, + // 查找最近的边(线段) + findNearestEdge(pointCoord, graph, nodeCoords) { + let nearestEdge = null + let minDistance = Infinity + let perpendicularPoint = null + // 为了兼容你最初 nodeCoords 结构(你之前用 coords[0][0]),下面我们从 feature.geometry.coordinates 里取实际线数组 + const features = this.roadNetworkGeoJSON.features - // 判断节点是否是死胡同(只有一个连接) - isDeadEnd(node, graph) { - if (!graph[node]) return true - const connections = Object.keys(graph[node]) - return connections.length === 1 - }, - - // 找到死胡同的入口节点(原路返回的衔接点) - findDeadEndEntry(deadEndNode, graph, fromNode, nodeCoords) { - // 从死胡同节点开始,沿着唯一路径往回找,直到找到分支点或起点 - let currentNode = deadEndNode - let visited = new Set([currentNode]) - - while (currentNode) { - const connections = graph[currentNode] ? Object.keys(graph[currentNode]) : [] - - // 如果当前节点有多个连接,或者是从起点过来的节点,作为入口点 - if (connections.length > 1 || currentNode === fromNode) { - return currentNode + for (const feature of features) { + // 支持 LineString 或 MultiLineString(取每条线段) + const geom = feature.geometry + let lines = [] + if (geom.type === 'LineString') { + lines = [geom.coordinates] + } else if (geom.type === 'MultiLineString') { + lines = geom.coordinates + } else { + continue } - // 继续往回找(排除已访问的节点) - const nextNode = connections.find((node) => !visited.has(node)) - if (!nextNode) break + for (const lineCoords of lines) { + // turf.nearestPointOnLine 需要 lineString(数组经纬),返回距离(单位:度->通过 properties.dist) + const line = turf.lineString(lineCoords) + const nearest = turf.nearestPointOnLine(line, turf.point(pointCoord), {units: 'meters'}) + // 注意:turf.nearestPointOnLine 返回的 properties.dist 是以图层单位(默认度),但在新版本通常有 `dist` (米) 根据 turf 版本会不同 + // 为保险,用 turf.distance 重新计算米级距离: + const proj = nearest.geometry.coordinates + const distMeters = turf.distance(turf.point(pointCoord), turf.point(proj), {units: 'meters'}) - visited.add(nextNode) - currentNode = nextNode - } - - // 如果找不到合适的入口,返回原始最近节点 - return deadEndNode - }, - - // 改进的路径查找,支持原路返回 - async findPathWithReturn(graph, startNode, endNode, nodeCoords) { - try { - // 先尝试直接路径 - const directPath = dijkstra.find_path(graph, startNode, endNode) - if (directPath && directPath.length > 0) { - return {pathNodes: directPath, isDeadEnd: false} - } - - // 如果直接路径失败,检查是否是死胡同情况 - if (this.isDeadEnd(endNode, graph)) { - // 找到死胡同入口作为替代终点 - const entryNode = this.findDeadEndEntry(endNode, graph, startNode, nodeCoords) - if (entryNode && entryNode !== endNode) { - const alternativePath = dijkstra.find_path(graph, startNode, entryNode) - if (alternativePath && alternativePath.length > 0) { - return { - pathNodes: alternativePath, - isDeadEnd: true, - actualEndNode: endNode, - entryNode: entryNode, - } + if (distMeters < minDistance) { + minDistance = distMeters + perpendicularPoint = proj + // 保存 edge 信息:使用 feature 的 FNODE_/TNODE_ 作为端点(同你 graph 的节点) + nearestEdge = { + feature, + lineCoords, + startNode: feature.properties.FNODE_, + endNode: feature.properties.TNODE_, + distance: distMeters, } } } + } - return {pathNodes: null, isDeadEnd: false} + if (!nearestEdge) return null + + return { + startNode: nearestEdge.startNode, + endNode: nearestEdge.endNode, + line: turf.lineString(nearestEdge.lineCoords), + distance: nearestEdge.distance, + perpendicularPoint: perpendicularPoint, + tempNodeId: `temp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + feature: nearestEdge.feature, + lineCoords: nearestEdge.lineCoords, + } + }, + + // 计算垂直连接信息(保持原结构) + calculatePerpendicularConnection(point, edgeInfo, type = 'start') { + // type: 'start' | 'via' | 'end' + return { + originalPoint: point, + perpendicularPoint: edgeInfo.perpendicularPoint, + edgeStart: edgeInfo.startNode, + edgeEnd: edgeInfo.endNode, + tempNodeId: edgeInfo.tempNodeId, + distance: edgeInfo.distance, + type, // 新增标记,后续拼接方向要用 + feature: edgeInfo.feature, + lineCoords: edgeInfo.lineCoords, + } + }, + // 构建包含垂直连接点的扩展图(改进:会把临时节点连到对应的两个端点,权重为米级距离) + buildExtendedGraph(originalGraph, nodeCoords, startConnection, endConnection) { + // 深拷贝原图 + const extendedGraph = JSON.parse(JSON.stringify(originalGraph)) + + const addTemp = (conn) => { + if (!conn) return + const tempId = conn.tempNodeId + extendedGraph[tempId] = extendedGraph[tempId] || {} + + // 取对应两个端点的经纬(nodeCoords 存储节点经纬) + const coordA = nodeCoords[conn.edgeStart] + const coordB = nodeCoords[conn.edgeEnd] + + // 如果 nodeCoords 中没有任意端点坐标,尝试从 feature geometry 取头尾 + if (!coordA || !coordB) { + const fcoords = conn.lineCoords + // 头尾可能需要调整,根据你的 data 结构,fcoords[0]/fcoords[fcoords.length-1] + if (!coordA) coordA = fcoords[0] + if (!coordB) coordB = fcoords[fcoords.length - 1] + } + + const dA = this.calculateDistance(conn.perpendicularPoint, coordA) + const dB = this.calculateDistance(conn.perpendicularPoint, coordB) + + // temp -> A/B + extendedGraph[tempId][conn.edgeStart] = dA + extendedGraph[tempId][conn.edgeEnd] = dB + + // A/B -> temp (保证无向双向) + if (!extendedGraph[conn.edgeStart]) extendedGraph[conn.edgeStart] = {} + if (!extendedGraph[conn.edgeEnd]) extendedGraph[conn.edgeEnd] = {} + extendedGraph[conn.edgeStart][tempId] = dA + extendedGraph[conn.edgeEnd][tempId] = dB + } + + addTemp(startConnection) + addTemp(endConnection) + + return extendedGraph + }, + + // 查找包含垂直连接的路径(你已有 dijkstra.find_path)保持不变 + async findPathWithPerpendicularConnections(graph, startNode, endNode) { + try { + const path = dijkstra.find_path(graph, startNode, endNode) + return path && path.length > 0 ? path : null } catch (error) { console.error('路径查找错误:', error) - return {pathNodes: null, isDeadEnd: false} + return null } }, - // 通过坐标反向查找对应的路网节点(处理浮点数精度) - findNodeByCoord(coord, nodeCoords) { - const tolerance = 0.000001 // 与findPointIndex保持一致的容差 - for (const [nodeId, nodeCoord] of Object.entries(nodeCoords)) { - if (Math.abs(nodeCoord[0] - coord[0]) < tolerance && Math.abs(nodeCoord[1] - coord[1]) < tolerance) { - return nodeId + + // 辅助:在一条线(lineCoords)上找到垂足点最近的索引位置(返回最近点索引) + // 如果垂足不在顶点上,会返回两个点之间最近的后续起点索引(便于切片) + findNearestIndexOnLine(lineCoords, perpPoint) { + let minDist = Infinity + let nearestIndex = 0 + for (let i = 0; i < lineCoords.length; i++) { + const d = turf.distance(turf.point(lineCoords[i]), turf.point(perpPoint), {units: 'meters'}) + if (d < minDist) { + minDist = d + nearestIndex = i } } - // 若未找到完全匹配的节点,返回最近的节点(降级处理) - return this.findNearestNode(coord, nodeCoords) + return {index: nearestIndex, distance: minDist} }, - // 改进的路径生成,支持原路返回 - async generatePathWithReturn( + + // 根据路径节点(node id 列表)生成最终的经纬路径(含垂足点) + // 说明:pathNodes 包含临时节点 temp_xxx,startConnection/endConnection 帮助在首尾插入垂足 + async generatePathWithPerpendicularConnections( pathNodes, segmentStart, segmentEnd, - segmentIndex, - totalPoints, - currPointType, - nextPointType, - isDeadEnd = false, - prevSegmentEndNode = null + startConnection, + endConnection, + graph ) { const segmentPath = [] const segments = [] + let hasValidSegment = false + // 新增:存储途经点垂直点之后的路网后半截(供后续路径延续) + let viaPerpRemainingCoords = [] - // 生成主要路径段 - for (let j = 0; j < pathNodes.length - 1; j++) { - const currentNode = pathNodes[j] - const nextNode = pathNodes[j + 1] + // 1. 起点 -> 垂足(直线) + if (startConnection.type !== 'via') { + // 起点 + segmentPath.push(segmentStart) + segmentPath.push(startConnection.perpendicularPoint) + // return { path: [], segments: [] }; + } else { + // 途径点 + segmentPath.push(segmentStart) + // segmentPath.push(startConnection.perpendicularPoint); + // return { path: [], segments: [] }; + } + // 2. 去掉 temp 节点,保留路网节点序列(但我们需要完整 pathNodes 来判断是否直接由 temp -> temp) + // 但在拼接线段时我们会遍历 pathNodes 的相邻 pair(包含真实节点) + for (let idx = 0; idx < pathNodes.length - 1; idx++) { + const curNode = pathNodes[idx] + const nextNode = pathNodes[idx + 1] + + // 如果任意一端是临时节点(temp_),跳过真实线段查找逻辑(临时节点已经在扩展图里用直连距离) + if (curNode.startsWith('temp_') || nextNode.startsWith('temp_')) { + // 当 cur 是 temp 且 next 是真实节点 -> 需要把垂足点连到 nextNode 所在路段的最近点 + // 但我们已在扩展图把 temp 与它相连的端点(edgeStart/edgeEnd)连接上了,所以这里可以直接跳过具体线段拼接 + // 为了保证路径的连续性,当 temp 后面直接接真实节点(如 A),我们应该把垂足点到 A 的连线在上一部分已经通过 extendedGraph 权重计算, + // 但这里仍需把真正的路段几何(feature)加入以便在地图上显示路段细节,故继续到下一部分的真实-真实节点处理 + continue + } + + // curNode 与 nextNode 都是真实节点:找到你路网 feature(可能有多段匹配,取第一个匹配)并决定方向 let segment = null 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) + (String(f.properties.FNODE_) === String(curNode) && String(f.properties.TNODE_) === String(nextNode)) || + (String(f.properties.FNODE_) === String(nextNode) && String(f.properties.TNODE_) === String(curNode)) ) } 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.FNODE_ == curNode && f.properties.TNODE_ == nextNode) || + (f.properties.FNODE_ == nextNode && f.properties.TNODE_ == curNode)) && f.properties.载重吨 >= this.inputform.load && f.properties.宽度 >= this.inputform.width && f.properties.曲率半 <= this.inputform.minTurnRadius ) } - if (!segment) continue + if (!segment) { + console.warn(`找不到符合条件的路段: ${curNode} -> ${nextNode}`) + // 若找不到对应路段(可能被避让删掉或数据不一致),则跳过 + continue + } - segments.push(segment) - const segmentCoords = segment.geometry.coordinates[0] + if (segment) { + hasValidSegment = true + segments.push(segment) + // 取线数组(LineString 或 MultiLine 的第一条) + let segCoords = [] + if (segment.geometry.type === 'LineString') { + segCoords = segment.geometry.coordinates + } else if (segment.geometry.type === 'MultiLineString') { + segCoords = segment.geometry.coordinates[0] + } - // 处理段内连接 - if (j === 0) { - await this.handleSegmentStartWithReturn( - segmentPath, - segmentStart, - segmentCoords, - segmentIndex, - currPointType, - prevSegmentEndNode - ) - } else { - this.connectSegmentInternally(segmentPath, segmentCoords) + // 确定方向:如果 FNODE_ === curNode,则线方向为 segCoords 正序,否则需要反转 + const isForward = String(segment.properties.FNODE_) === String(curNode) + const coordsToUse = isForward ? segCoords : [...segCoords].reverse() + + // === 使用第一个版本的精细连接逻辑,但加入第二个版本的途经点切片 === + const lastPt = segmentPath[segmentPath.length - 1] + const firstOfSeg = coordsToUse[0] + + const distLastToFirst = this.calculateDistance(lastPt, firstOfSeg) + + // 如果是途经点,需要找到终点垂足在当前位置 + const endPerpNearest = this.findNearestPointWithIndex(coordsToUse, endConnection.perpendicularPoint) + const endNi = endPerpNearest.index + + if (distLastToFirst < 1e-6) { + // 精度上相同,直接接上(跳过第一个) + // 如果是途经点,需要切片到垂足 + if (endConnection.type === 'via') { + segmentPath.push(...coordsToUse.slice(1, endNi + 1)) + // 保存垂直点之后的路网后半截 + viaPerpRemainingCoords = coordsToUse.slice(endNi) + } else { + segmentPath.push(...coordsToUse.slice(1)) + } + } else { + // 找到 coordsToUse 上与 lastPt 最近的索引 + const nearestInfo = this.findNearestPointWithIndex(coordsToUse, lastPt) + const ni = nearestInfo.index + + // === 整合第二个版本的途经点切片逻辑 === + if (endConnection.type === 'via') { + // 途经点处理:统一以终点垂足为切片终点 + if (ni <= endNi) { + segmentPath.push(...coordsToUse.slice(ni, endNi + 1)) + // 关键:保存垂直点之后的路网后半截(供后续路径延续) + viaPerpRemainingCoords = coordsToUse.slice(endNi) + } else { + const reversedSlice = coordsToUse.slice(endNi, ni + 1).reverse() + segmentPath.push(...reversedSlice) + // 关键:保存垂直点之后的路网后半截(反向场景需要反转回去) + const originalRemaining = coordsToUse.slice(endNi) + viaPerpRemainingCoords = originalRemaining.reverse() + } + } else { + // 非途经点:使用第一个版本的完整连接逻辑 + if (ni === 0) { + // 从头开始接(直接接) + segmentPath.push(...coordsToUse) + } else if (ni === coordsToUse.length - 1) { + // 最近点是段尾 —— 说明我们需要反向接(取反转) + const rev = [...coordsToUse].reverse() + // 以 rev 的第一个点连接 + if (this.calculateDistance(lastPt, rev[0]) < 1e-6) { + segmentPath.push(...rev.slice(1)) + } else { + // 否则直接把最近点加入并向最近端延伸(避免断链) + segmentPath.push(coordsToUse[ni]) + // 选择靠近终点的方向延伸(更短的一侧) + const distToStart = this.calculateDistance(coordsToUse[ni], coordsToUse[0]) + const distToEnd = this.calculateDistance(coordsToUse[ni], coordsToUse[coordsToUse.length - 1]) + if (distToStart <= distToEnd) { + const toStart = coordsToUse.slice(0, ni).reverse() + segmentPath.push(...toStart) + } else { + const toEnd = coordsToUse.slice(ni + 1) + segmentPath.push(...toEnd) + } + } + } else { + // 最近点在中间:选择向起点或终点延伸,取较短的一侧 + segmentPath.push(coordsToUse[ni]) + const distToStart = this.calculateDistance(coordsToUse[ni], coordsToUse[0]) + const distToEnd = this.calculateDistance(coordsToUse[ni], coordsToUse[coordsToUse.length - 1]) + if (distToStart <= distToEnd) { + const toStart = coordsToUse.slice(0, ni).reverse() + segmentPath.push(...toStart) + } else { + const toEnd = coordsToUse.slice(ni + 1) + segmentPath.push(...toEnd) + } + } + } + } } } + // 如果没有任何有效的路段,返回空路径 + if (!hasValidSegment) { + this.$message.warning('整段路径中没有找到任何符合条件的路段') + return {path: [], segments: []} + } + // 3. 最后加上终点垂足 -> 终点 的直线 + // === 处理终点和途径点的垂足连线 === + // 确保终点/途径点垂足处理 + if (endConnection.type === 'end') { + const endLineCoords = endConnection.lineCoords + const lastPt = segmentPath[segmentPath.length - 1] - // 处理段结束连接(支持死胡同原路返回) - await this.handleSegmentEndWithReturn( - segmentPath, - segmentEnd, - pathNodes, - segmentIndex, - totalPoints, - nextPointType, - isDeadEnd - ) + // 找到路径末尾点在线段上最近的点索引 + const nearestToLast = this.findNearestPointWithIndex(endLineCoords, lastPt) + const nearestToPerp = this.findNearestPointWithIndex(endLineCoords, endConnection.perpendicularPoint) + let subLine + if (nearestToLast.index <= nearestToPerp.index) { + // 从 lastPt -> 垂足方向 + subLine = endLineCoords.slice(nearestToLast.index, nearestToPerp.index + 1) + } else { + // 反向 + subLine = endLineCoords.slice(nearestToPerp.index, nearestToLast.index + 1).reverse() + } + + // 将最近线段拼到路径末尾(确保方向合理) + this.connectSegmentInternally(segmentPath, subLine) + + // 最后垂足 -> 点 + segmentPath.push(endConnection.perpendicularPoint) + segmentPath.push(segmentEnd) + } else if (endConnection.type === 'via') { + segmentPath.push(endConnection.perpendicularPoint) + segmentPath.push(segmentEnd) + segmentPath.push(endConnection.perpendicularPoint) + // 结束点为途经点: + // a. 先添加垂直点后续的路网后半截(供下一段路径延续) + if (viaPerpRemainingCoords.length > 1) { + // 跳过第一个点(与垂直点重复),添加后续部分 + segmentPath.push(...viaPerpRemainingCoords.slice(1)) + } + } else { + // 默认逻辑 + segmentPath.push(endConnection.perpendicularPoint) + segmentPath.push(segmentEnd) + } + if (startConnection.type == 'via' && segmentPath.length > 0) { + segmentPath.shift() // 移除数组第一个元素 + } return {path: segmentPath, segments} }, - // 改进的段起始处理 - async handleSegmentStartWithReturn( - segmentPath, - segmentStart, - segmentCoords, - segmentIndex, - currPointType, - prevSegmentEndNode - ) { - if (segmentIndex === 0 || currPointType === 'start') { - // 第一段或起点:从实际起点开始 - segmentPath.push(segmentStart) - const nearestPoint = this.findNearestPointOnLine(segmentStart, segmentCoords) - segmentPath.push(nearestPoint) - - const startIndex = this.findPointIndex(segmentCoords, nearestPoint) - if (startIndex !== -1 && startIndex < segmentCoords.length - 1) { - segmentPath.push(...segmentCoords.slice(startIndex + 1)) - } - } else { - // 途经点之后的段:确保路径连续性 - segmentPath.push(segmentStart) - - // 找到与上一段衔接的最佳点 - const connectionPoint = this.findContinuationPoint(segmentCoords, prevSegmentEndNode) - if (connectionPoint) { - segmentPath.push(connectionPoint) - const connectionIndex = this.findPointIndex(segmentCoords, connectionPoint) - if (connectionIndex !== -1 && connectionIndex < segmentCoords.length - 1) { - segmentPath.push(...segmentCoords.slice(connectionIndex + 1)) - } - } else { - // 降级处理:使用最近点 - const nearestPoint = this.findNearestPointOnLine(segmentStart, segmentCoords) - segmentPath.push(nearestPoint) - const startIndex = this.findPointIndex(segmentCoords, nearestPoint) - if (startIndex !== -1 && startIndex < segmentCoords.length - 1) { - segmentPath.push(...segmentCoords.slice(startIndex + 1)) - } - } - } - }, - - // 改进的段结束处理(支持死胡同) - async handleSegmentEndWithReturn( - segmentPath, - segmentEnd, - pathNodes, - segmentIndex, - totalPoints, - nextPointType, - isDeadEnd - ) { - if (isDeadEnd) { - // 死胡同情况:先到达目标点,然后原路返回到入口点 - const lastRoadNode = pathNodes[pathNodes.length - 1] - const nearestPoint = this.findBestConnectionPoint(segmentEnd, lastRoadNode) - - if (nearestPoint) { - // 前往目标点 - segmentPath.push(nearestPoint) - segmentPath.push(segmentEnd) - - // 原路返回到入口点(反向路径) - this.retracePath(segmentPath, pathNodes) - } - } else { - // 正常情况 - if (segmentIndex === totalPoints - 2) { - // 最后一段:连接到实际终点 - const lastRoadNode = pathNodes[pathNodes.length - 1] - const nearestPoint = this.findBestConnectionPoint(segmentEnd, lastRoadNode) - - if (nearestPoint) { - segmentPath.push(nearestPoint) - segmentPath.push(segmentEnd) - } - } else { - // 中间段:途经点处理 - const lastRoadNode = pathNodes[pathNodes.length - 1] - const nearestPoint = this.findBestConnectionPoint(segmentEnd, lastRoadNode) - - if (nearestPoint) { - segmentPath.push(nearestPoint) - segmentPath.push(segmentEnd) - } - } - } - }, - - // 原路返回逻辑 - retracePath(segmentPath, pathNodes) { - // 从路径节点反向生成返回路径 - for (let i = pathNodes.length - 2; i >= 0; i--) { - const currentNode = pathNodes[i] - const nextNode = pathNodes[i + 1] - - let segment = this.roadNetworkGeoJSON.features.find( - (f) => - (f.properties.FNODE_ == currentNode && f.properties.TNODE_ == nextNode) || - (f.properties.FNODE_ == nextNode && f.properties.TNODE_ == currentNode) - ) - - if (!segment) continue - - const segmentCoords = [...segment.geometry.coordinates[0]].reverse() - this.connectSegmentInternally(segmentPath, segmentCoords) - } - }, - - // 找到路径延续点(确保路径连续性) - findContinuationPoint(segmentCoords, prevEndNode) { - if (!prevEndNode) return null - - const nodeCoords = this.roadNetworkGeoJSONBuild.nodeCoords[prevEndNode] - if (!nodeCoords) return null - - // 在路段上找到与上一段结束点最近的点 - return this.findNearestPointOnLine(nodeCoords, segmentCoords) - }, - - // 处理段起始连接 - async handleSegmentStart(segmentPath, segmentStart, segmentCoords, segmentIndex) { - if (segmentIndex === 0) { - // 第一段:从实际起点开始 - segmentPath.push(segmentStart) - - // 找到距离起点最近的道路点 - const nearestPoint = this.findNearestPointOnLine(segmentStart, segmentCoords) - segmentPath.push(nearestPoint) - - // 添加从最近点到路径的剩余部分 - const startIndex = this.findPointIndex(segmentCoords, nearestPoint) - if (startIndex !== -1 && startIndex < segmentCoords.length - 1) { - segmentPath.push(...segmentCoords.slice(startIndex + 1)) - } - } else { - // 中间段(途经点之后):从途经点开始 - segmentPath.push(segmentStart) - - // 找到距离途经点最近的道路点 - const nearestPoint = this.findNearestPointOnLine(segmentStart, segmentCoords) - segmentPath.push(nearestPoint) - - // 添加从最近点到路径的剩余部分 - const startIndex = this.findPointIndex(segmentCoords, nearestPoint) - if (startIndex !== -1 && startIndex < segmentCoords.length - 1) { - segmentPath.push(...segmentCoords.slice(startIndex + 1)) - } else { - // 如果找不到索引,直接添加整段路径 - segmentPath.push(...segmentCoords) - } - } - }, - - // 处理段结束连接 - async handleSegmentEnd(segmentPath, segmentEnd, pathNodes, segmentIndex, totalPoints) { - if (segmentIndex === totalPoints - 2) { - // 最后一段:连接到实际终点 - const lastRoadNode = pathNodes[pathNodes.length - 1] - const nearestPoint = this.findBestConnectionPoint(segmentEnd, lastRoadNode) - - if (nearestPoint) { - segmentPath.push(nearestPoint) - segmentPath.push(segmentEnd) - } - } else { - // 中间段:途经点处理 - 确保正确连接到途经点 - const lastRoadNode = pathNodes[pathNodes.length - 1] - const nearestPoint = this.findBestConnectionPoint(segmentEnd, lastRoadNode) - - if (nearestPoint) { - // 先连接到路径上的最近点 - segmentPath.push(nearestPoint) - // 然后连接到途经点本身 - segmentPath.push(segmentEnd) - } - } - }, - - // 合并路径段(避免重复点) - mergePathSegments(fullPath, newSegment) { - if (newSegment.length === 0) return - - if (fullPath.length === 0) { - fullPath.push(...newSegment) - return - } - - const lastPoint = fullPath[fullPath.length - 1] - const firstNewPoint = newSegment[0] - - // 检查是否需要连接(如果点不重复) - if (lastPoint[0] !== firstNewPoint[0] || lastPoint[1] !== firstNewPoint[1]) { - // 点不重复,直接添加 - fullPath.push(...newSegment) - } else { - // 点重复,跳过第一个点 - if (newSegment.length > 1) { - fullPath.push(...newSegment.slice(1)) - } - } - }, - - // 在线段上找到最近的点 - findNearestPointOnLine(point, lineCoords) { - let nearestPoint = lineCoords[0] - let minDistance = this.calculateDistance(point, nearestPoint) - - for (let i = 1; i < lineCoords.length; i++) { - const coord = lineCoords[i] - const distance = this.calculateDistance(point, coord) - if (distance < minDistance) { - minDistance = distance - nearestPoint = coord - } - } - - return nearestPoint - }, - - // 查找点在数组中的索引 - findPointIndex(coords, point) { - const tolerance = 0.000001 // 容差,处理浮点数精度问题 - - for (let i = 0; i < coords.length; i++) { - if (Math.abs(coords[i][0] - point[0]) < tolerance && Math.abs(coords[i][1] - point[1]) < tolerance) { - return i - } - } - return -1 - }, - - // 找到最佳连接点 - findBestConnectionPoint(actualPoint, roadNode) { - const roadNodeCoord = this.roadNetworkGeoJSONBuild.nodeCoords[roadNode] - if (!roadNodeCoord) return null - - // 查找连接到该节点的所有道路段 - const connectedRoads = this.roadNetworkGeoJSON.features.filter( - (road) => road.properties.FNODE_ === roadNode || road.properties.TNODE_ === roadNode - ) - - if (connectedRoads.length === 0) return roadNodeCoord - - let bestPoint = roadNodeCoord - let minDistance = this.calculateDistance(actualPoint, roadNodeCoord) - - // 在所有连接的道路段上寻找最佳连接点 - connectedRoads.forEach((road) => { - const coords = road.geometry.coordinates[0] - - // 检查道路段的每个点 - coords.forEach((coord) => { - const distance = this.calculateDistance(actualPoint, coord) - if (distance < minDistance) { - minDistance = distance - bestPoint = coord - } - }) - }) - - return bestPoint - }, // 段内小段连接 connectSegmentInternally(segmentPath, segmentCoords) { @@ -1974,30 +2080,6 @@ export default { const point2 = turf.point(coord2) return turf.distance(point1, point2, {units: 'meters'}) }, - // 检查路径可行性 - isPathPossible(graph, startNode, endNode) { - // 使用广度优先搜索(BFS)检查路径可行性 - const visited = new Set() - const queue = [startNode] - visited.add(startNode) - - while (queue.length > 0) { - const currentNode = queue.shift() - if (currentNode === endNode) { - return true - } - const neighbors = graph[currentNode] - if (neighbors) { - for (const neighbor in neighbors) { - if (!visited.has(neighbor)) { - visited.add(neighbor) - queue.push(neighbor) - } - } - } - } - return false - }, async calculateShortestPath() { if (!this.pointQD || !this.pointZD || !this.roadNetworkGeoJSON) { this.$message.warning('请先加载路网数据并绘制障碍面、起点和终点!') @@ -2046,23 +2128,6 @@ export default { 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) - // }, // 单独的路径绘制方法 - 添加箭头 drawPath(path) { const positions = path @@ -2207,6 +2272,204 @@ export default { return [tip, left, right, tip] // 闭合三角形 }, + /** 右侧表单导出 */ + handleExport() { + if (this.infoList && this.infoList.length == 0) return + const hideData = [] + if (this.factoriesWithVehicles.length > 0) { + this.factoriesWithVehicles.forEach((item, index) => [ + hideData.push({ + FID_1: item.options.style.properties.FID_1, + vehiclesNum: item.vehicles.map((e) => e.name).join(','), + area: item.area.toFixed(2), + id: index, + }), + ]) + } + this.hideData = JSON.parse(JSON.stringify(hideData)) + const routeData = [] + if (this.infoList.length > 0) { + this.infoList.forEach((item, index) => [ + routeData.push({ + 编码: item.编码, + 名称: item.名称, + 宽度: item.宽度, + 曲率半: item.曲率半, + 载重吨: item.载重吨, + 水深: item.水深, + 净空高: item.净空高, + id: index, + }), + ]) + } + this.routeData = JSON.parse(JSON.stringify(routeData)) + this.dialogExportVisible = true + }, + /** 增删改查 */ + handleRouteAdd() { + const newRow = { + 编码: null, + 名称: null, + 宽度: null, + 曲率半: null, + 载重吨: null, + 水深: null, + 净空高: null, + editing: true, + id: this.routeData.length + 1, + } + this.routeData.push(newRow) + // 等待数据更新后滚动到新增的行 + setTimeout(() => { + this.$refs.routeTable.refreshScroll() // 刷新滚动 + this.$refs.routeTable.scrollToRow(newRow, 'id') + }, 50) + }, + handleRouteDelete(row) { + const index = this.routeData.findIndex((item) => item.id === row.id) + if (index !== -1) { + this.routeData.splice(index, 1) + } + }, + handleRouteEdit(row) { + this.$set(row, 'editing', true) + }, + handleRouteSave(row) { + this.$set(row, 'editing', false) + }, + handleHideAdd() { + const newRow = { + id: this.hideData.length + 1, + FID_1: '', + vehiclesNum: '', + area: '', + editing: true, + } + this.hideData.push(newRow) + // 等待数据更新后滚动到新增的行 + setTimeout(() => { + this.$refs.hideTable.refreshScroll() // 刷新滚动 + this.$refs.hideTable.scrollToRow(newRow, 'id') + }, 50) + }, + handleHideDelete(row) { + const index = this.hideData.findIndex((item) => item.id === row.id) + if (index !== -1) { + this.hideData.splice(index, 1) + } + }, + handleHideEdit(row) { + this.$set(row, 'editing', true) + }, + handleHideSave(row) { + this.$set(row, 'editing', false) + }, + closeExport() { + this.dialogExportVisible = false + }, + confirmExport() { + this.hideData = this.hideData.map((item) => { + const {editing, ...rest} = item // 解构赋值,移除 age 键 + return rest + }) + this.routeData = this.routeData.map((item) => { + const {editing, ...rest} = item // 解构赋值,移除 age 键 + return rest + }) + const info = JSON.stringify( + { + hideData: this.hideData, + routeData: this.routeData, + }, + null, + 2 + ) + // const blob = new Blob([info], { type: 'application/json;charset=utf-8' }) + // saveAs(blob, '机动路线规划.json') + // this.closeExport() + fetch('./config.ini') + .then((response) => response.text()) + .then((text) => { + const parsedData = iniParser.parse(text) + axios + .post(`http://${parsedData.http.address}:${parsedData.http.port}/api/route`, info, { + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + this.$message.success('导出成功') + }) + .catch((error) => {}) + }) + }, + /** json编辑 */ + handleExportJosn() { + this.dialogJsonVisible = true + }, + decodeEscapedJson(str) { + // 去掉字符串首尾的空白字符 + str = str.trim() + // 直接解析 JSON 字符串 + try { + return JSON.parse(str) + } catch (e) { + console.error('Error parsing JSON:', e) + return null + } + }, + handleJson() { + this.jsonLoading = true + fetch('./config.ini') + .then((response) => response.text()) + .then((text) => { + const parsedData = iniParser.parse(text) + axios + .post(`http://${parsedData.http.address}:${parsedData.http.port}/api/json/select`, { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 10 * 60 * 1000, + }) + .then((response) => { + this.jsonInfo = { + path: response.data.path, + json: JSON.stringify(response.data.json, null, 2), + } + this.jsonLoading = false + }) + .catch((error) => { + this.jsonLoading = false + }) + }) + }, + closeJson() { + this.jsonInfo.path = '' + this.jsonInfo.json = '' + this.dialogJsonVisible = false + }, + confirmJson() { + fetch('./config.ini') + .then((response) => response.text()) + .then((text) => { + const parsedData = iniParser.parse(text) + axios + .post( + `http://${parsedData.http.address}:${parsedData.http.port}/api/json/save?path=${this.jsonInfo.path}`, + JSON.stringify(JSON.parse(this.jsonInfo.json)), + { + headers: { + 'Content-Type': 'application/json', + }, + } + ) + .then((response) => { + this.$message.success('保存成功') + this.closeJson() + }) + .catch((error) => {}) + }) + }, }, } @@ -2217,10 +2480,17 @@ export default { line-height: 60px; display: flex; align-items: center; - padding-left: 34px; + justify-content: space-between; + padding: 0 34px; box-sizing: border-box; background: #abc6bc; } +.home-header-left, +.home-header-right { + display: flex; + align-items: center; + box-sizing: border-box; +} .home-header img { height: 24px; width: 24px; @@ -2337,4 +2607,12 @@ export default { .popDiloag .popDiloag-title { font-weight: 500; } +.popDiloag .popDiloag-p { +} +::v-deep .el-dialog__body { + padding: 0px 20px !important; +} +::v-deep .el-dialog { + margin-top: 6vh !important; +} diff --git a/src/views/home/home202510115.vue b/src/views/home/home202510115.vue new file mode 100644 index 0000000..eb72b7d --- /dev/null +++ b/src/views/home/home202510115.vue @@ -0,0 +1,2616 @@ + + + + +