From 41f9034f540f334d1d97da496bce0de075689c88 Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Fri, 10 Oct 2025 11:38:34 +0800 Subject: [PATCH 1/6] =?UTF-8?q?##=20=E6=9C=BA=E5=8A=A8=E8=B7=AF=E7=BA=BF?= =?UTF-8?q?=E8=A7=84=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) 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/). +## 机动路线规划 From 2ad6bc1b474df9bfd06d2069a5c5047bba75c8ea Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Fri, 10 Oct 2025 15:32:47 +0800 Subject: [PATCH 2/6] =?UTF-8?q?add=20=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + src/views/home/home.vue | 245 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 236 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 7f9a28f..73c9c6e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "core-js": "^3.8.3", "echarts": "^5.4.3", "element-ui": "2.9.2", + "file-saver": "^2.0.5", "gsap": "^3.13.0", "ini-parser": "^0.0.2", "js-cookie": "2.2.1", diff --git a/src/views/home/home.vue b/src/views/home/home.vue index a1c80ab..5d31ebc 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -1,15 +1,17 @@ @@ -249,6 +360,7 @@ import Cookies from 'js-cookie' import axios from 'axios' import iniParser from 'ini-parser' import configIni from '/config.ini'; +import { saveAs } from 'file-saver'; export default { data() { @@ -294,6 +406,9 @@ export default { factoriesWithVehicles: [], // 塞入车的厂房集合 accordFactoryLayer: null, // 隐蔽规划 accordPoint: null, // 隐蔽规划点 + dialogExportVisible: false, + routeData: [], + hideData: [], } }, async mounted() { @@ -2274,6 +2389,104 @@ export default { return [tip, left, right, tip] // 闭合三角形 }, + /** 右侧表单导出 */ + handleExport() { + const hideData = [] + if (this.factoriesWithVehicles.length > 0) { + this.factoriesWithVehicles.forEach((item) => [ + hideData.push({ + FID_1: item.options.style.properties.FID_1, + vehiclesNum: (item.vehicles.map(e => e.name)).join(','), + area: item.area.toFixed(2) + }) + ]) + } + this.hideData = JSON.parse(JSON.stringify(hideData)) + const routeData = [] + if (this.infoList.length > 0) { + this.infoList.forEach((item) => [ + routeData.push({ + 编码: item.编码, + 名称: item.名称, + 宽度: item.宽度, + 曲率半: item.曲率半, + 载重吨: item.载重吨, + 水深: item.水深, + 净空高: item.净空高, + }) + ]) + } + this.routeData = JSON.parse(JSON.stringify(routeData)) + this.dialogExportVisible = true + }, + /** 增删改查 */ + handleRouteAdd() { + const newRow = { + 编码: null, + 名称: null, + 宽度: null, + 曲率半: null, + 载重吨: null, + 水深: null, + 净空高: null, + editing: true, + }; + this.routeData.push(newRow); + }, + 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); + }, + 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() + }, }, } @@ -2284,10 +2497,16 @@ 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 { + display: flex; + align-items: center; + box-sizing: border-box; +} .home-header img { height: 24px; width: 24px; @@ -2406,4 +2625,10 @@ export default { } .popDiloag .popDiloag-p { } +::v-deep .el-dialog__body{ + padding: 0px 20px!important; +} +::v-deep .el-dialog { + margin-top: 6vh !important; +} From 328700ec8946f8a7f17c1bc68e0a90e5eb5c17d5 Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Fri, 10 Oct 2025 18:31:44 +0800 Subject: [PATCH 3/6] fix --- config.ini => public/config.ini | 2 +- src/views/home/home.vue | 74 +++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 24 deletions(-) rename config.ini => public/config.ini (84%) diff --git a/config.ini b/public/config.ini similarity index 84% rename from config.ini rename to public/config.ini index 4bcabac..b55b302 100644 --- a/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 5d31ebc..50cf6b1 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -252,7 +252,7 @@ 新增 新增 {}) }, async initMap() { - const parsedData = iniParser.parse(configIni); this.viewer = new window.mars3d.Map( 'map', { @@ -1018,18 +1015,21 @@ 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('保存成功') - }) - .catch(error => { - }); + 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 => { + }); + }); }, // 导入json文件 triggerFileUpload() { @@ -2393,18 +2393,19 @@ export default { handleExport() { const hideData = [] if (this.factoriesWithVehicles.length > 0) { - this.factoriesWithVehicles.forEach((item) => [ + 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) + 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) => [ + this.infoList.forEach((item, index) => [ routeData.push({ 编码: item.编码, 名称: item.名称, @@ -2413,6 +2414,7 @@ export default { 载重吨: item.载重吨, 水深: item.水深, 净空高: item.净空高, + id: index }) ]) } @@ -2430,8 +2432,14 @@ export default { 水深: 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); @@ -2454,6 +2462,11 @@ export default { 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); @@ -2483,9 +2496,24 @@ export default { hideData: this.hideData, routeData: this.routeData, }, null, 2) - const blob = new Blob([info], { type: 'application/json;charset=utf-8' }) - saveAs(blob, '机动路线规划.json') - this.closeExport() + // 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 => { + }); + }); }, }, } From 7d22506f18ed0eb5325d744bee4bc394cab788b7 Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Tue, 14 Oct 2025 17:23:54 +0800 Subject: [PATCH 4/6] =?UTF-8?q?add=20json=20=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 ++- src/views/home/home.vue | 98 +++++++++++++++++++++++++++++++++++++++-- vue.config.js | 68 +++++++++++++++------------- 3 files changed, 136 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 73c9c6e..d32a75f 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "@babel/preset-env": "^7.28.3", "axios": "0.21.0", - "core-js": "^3.8.3", "echarts": "^5.4.3", "element-ui": "2.9.2", "file-saver": "^2.0.5", @@ -25,13 +25,15 @@ "vxe-table": "~3.18.9" }, "devDependencies": { - "@babel/core": "^7.12.16", + "@babel/core": "^7.28.4", "@babel/eslint-parser": "^7.12.16", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-vuex": "~5.0.0", "@vue/cli-service": "~5.0.0", + "babel-loader": "^10.0.0", + "core-js": "^3.46.0", "eslint": "^7.32.0", "eslint-config-prettier": "^8.3.0", "eslint-plugin-prettier": "^4.0.0", diff --git a/src/views/home/home.vue b/src/views/home/home.vue index 50cf6b1..1db1142 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -22,7 +22,8 @@ -->
-
导出
+
导出
+
json编辑
@@ -139,7 +140,13 @@
数据选择
-
+
+
@@ -351,6 +358,19 @@ 导出
+ + +
+
+ 选择 +
+ +
+ +
@@ -363,6 +383,7 @@ import iniParser from 'ini-parser' export default { data() { return { + mapLoading: false, dialogVisible: false, tableData: [], multipleSelection: [], @@ -407,6 +428,12 @@ export default { dialogExportVisible: false, routeData: [], hideData: [], + dialogJsonVisible: false, + jsonInfo: { + json: '', + path: '' + }, + jsonLoading: false } }, async mounted() { @@ -434,6 +461,7 @@ export default { .catch((error) => {}) }, async initMap() { + this.mapLoading = true this.viewer = new window.mars3d.Map( 'map', { @@ -565,6 +593,7 @@ export default { this.graphicLayer.addGraphic(graphicLine); }) this.roadNetworkGeoJSONBuild = this.buildGraph(this.roadNetworkGeoJSON) + this.mapLoading = false this.loadFactoryGeoJson() // 拿到厂房数据 } catch (error) { console.error('加载 Shapefile 数据失败:', error) @@ -2391,6 +2420,7 @@ export default { }, /** 右侧表单导出 */ handleExport() { + if (this.infoList && this.infoList.length == 0) return const hideData = [] if (this.factoriesWithVehicles.length > 0) { this.factoriesWithVehicles.forEach((item, index) => [ @@ -2515,6 +2545,67 @@ export default { }); }); }, + /** json编辑 */ + handleExportJosn() { + this.dialogJsonVisible = true + }, + decodeEscapedJson(str) { + // 去掉最外层的转义引号 + str = str.slice(1, -1); + // 循环解码,直到字符串不再包含转义字符 + while (str.includes('\\')) { + str = JSON.parse('"' + str + '"'); + } + return str + }, + 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: this.decodeEscapedJson(response.data.json) + } + 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(this.jsonInfo.json), { + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => { + this.$message.success('保存成功') + this.closeJson() + }) + .catch(error => { + }); + }); + + }, }, } @@ -2530,7 +2621,8 @@ export default { box-sizing: border-box; background: #abc6bc; } -.home-header-left { +.home-header-left, +.home-header-right { display: flex; align-items: center; box-sizing: border-box; diff --git a/vue.config.js b/vue.config.js index c50cad2..4407114 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,46 +1,52 @@ const {defineConfig} = require('@vue/cli-service') const postcssPxToViewport = require('postcss-px-to-viewport') -const path = require('path') // 需要引入 path 模块 +const path = require('path') module.exports = defineConfig({ publicPath: './', - transpileDependencies: false, + transpileDependencies: false, // 我们自己管,不让 CLI 插手 lintOnSave: false, - devServer: { - client: { - overlay: false, - }, - }, - configureWebpack: (config) => { - //调试JS + devServer: {client: {overlay: false}}, + + configureWebpack(config) { + // 调试 config.devtool = 'source-map' - config.resolve = { - alias: { - '@': path.resolve(__dirname, 'src'), + // 别名 + config.resolve.alias['@'] = path.resolve(__dirname, 'src') + + /* 1. 追加 ini 规则 */ + config.module.rules.push({ + test: /\.ini$/, + use: 'raw-loader', + }) + + /* 2. 追加 ES5 规则(只转 src,不转 node_modules) */ + config.module.rules.push({ + test: /\.js$/, + include: path.resolve(__dirname, 'src'), + use: { + loader: 'babel-loader', + options: { + presets: [ + [ + '@babel/preset-env', + { + targets: {ie: '11'}, // 强制 ES5 + corejs: 3, + useBuiltIns: 'entry', + }, + ], + ], + }, }, - } - // 添加 .ini 文件处理规则 - if (config.module && config.module.rules) { - config.module.rules.push({ - test: /\.ini$/, - use: 'raw-loader' - }) - } else { - config.module = { - rules: [ - { - test: /\.ini$/, - use: 'raw-loader' - } - ] - } - } + }) }, + css: { + /* 你的 px-to-viewport 配置不动 */ loaderOptions: { postcss: { postcssOptions: { - // 增加这一层 postcssOptions plugins: [ postcssPxToViewport({ unitToConvert: 'px', @@ -53,7 +59,7 @@ module.exports = defineConfig({ minPixelValue: 1, mediaQuery: false, replace: true, - exclude: /(\/|\\)(node_modules)(\/|\\)/, + exclude: /node_modules/, include: [], landscape: false, }), From ce70fd98ec6ef38a6dfc377235576cd533d2fca8 Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Wed, 15 Oct 2025 09:27:20 +0800 Subject: [PATCH 5/6] =?UTF-8?q?add=20json=E7=BC=96=E8=BE=91=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/home/home.vue | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/views/home/home.vue b/src/views/home/home.vue index 1db1142..2d1f45b 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -2549,14 +2549,16 @@ export default { handleExportJosn() { this.dialogJsonVisible = true }, - decodeEscapedJson(str) { - // 去掉最外层的转义引号 - str = str.slice(1, -1); - // 循环解码,直到字符串不再包含转义字符 - while (str.includes('\\')) { - str = JSON.parse('"' + str + '"'); + decodeEscapedJson(str) { + // 去掉字符串首尾的空白字符 + str = str.trim(); + // 直接解析 JSON 字符串 + try { + return JSON.parse(str); + } catch (e) { + console.error('Error parsing JSON:', e); + return null; } - return str }, handleJson() { this.jsonLoading = true @@ -2573,7 +2575,7 @@ export default { .then(response => { this.jsonInfo = { path: response.data.path, - json: this.decodeEscapedJson(response.data.json) + json: JSON.stringify(response.data.json, null, 2) } this.jsonLoading = false }) @@ -2592,7 +2594,7 @@ export default { .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(this.jsonInfo.json), { + 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' } From 964ce7cede7cb22726cac458346edf1c0c115544 Mon Sep 17 00:00:00 2001 From: gcw_IJ7DAiVL Date: Thu, 16 Oct 2025 16:02:23 +0800 Subject: [PATCH 6/6] =?UTF-8?q?fix=20bug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/home/home.vue | 857 +++++----- src/views/home/home202510115.vue | 2616 ++++++++++++++++++++++++++++++ 2 files changed, 3008 insertions(+), 465 deletions(-) create mode 100644 src/views/home/home202510115.vue diff --git a/src/views/home/home.vue b/src/views/home/home.vue index 2d1f45b..984a75b 100644 --- a/src/views/home/home.vue +++ b/src/views/home/home.vue @@ -580,17 +580,42 @@ 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 @@ -1604,488 +1629,431 @@ 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 startEdgeInfo = this.findNearestEdge(segmentStart, tempGraph, nodeCoords); + let endEdgeInfo = this.findNearestEdge(segmentEnd, tempGraph, nodeCoords); - // 匹配最近节点:关键修改——若当前是“途经点之后的段”,复用前一段的衔接点作为起点 - let startNode, endNode; - if (currPoint.type === 'via' && prevSegmentEndNode ) { - // 直接使用上一段的结束节点作为起点(确保路径连续性) - startNode = prevSegmentEndNode; - } else { - // 情况2:起点/第一段,正常匹配最近节点 - startNode = this.findNearestNode(segmentStart, nodeCoords); - } - endNode = this.findNearestNodeWithReturn(segmentEnd, nodeCoords, tempGraph, startNode); - - 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); + + // 构建包含垂直连接点的临时图 + const extendedGraph = this.buildExtendedGraph(tempGraph, nodeCoords, startConnection, endConnection); - // 检查路径可行性 - if (!this.isPathPossible(tempGraph, startNode, endNode)) { - this.$message.warning(`无法匹配到路网节点`); - continue; - } - - // 检查路径可行性(考虑死胡同原路返回) - 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); - }, - - // 判断节点是否是死胡同(只有一个连接) - 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; - } - - // 继续往回找(排除已访问的节点) - const nextNode = connections.find(node => !visited.has(node)); - if (!nextNode) break; - - visited.add(nextNode); - currentNode = nextNode; + const lastPoint = fullPath[fullPath.length - 1]; + const firstNewPoint = newSegment[0]; + const dist = this.calculateDistance(lastPoint, firstNewPoint); + if (dist > 1e-2) { + // 如果断开了超过1cm,就补上连接线 + fullPath.push(firstNewPoint); } - - // 如果找不到合适的入口,返回原始最近节点 - return deadEndNode; + 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; - // 改进的路径查找,支持原路返回 - 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}; + 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; } - - // 如果直接路径失败,检查是否是死胡同情况 - 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 - }; - } + + 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'}); + + 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 + }; } } + } + + 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; + }, + - return {pathNodes: null, isDeadEnd: false}; + // 查找包含垂直连接的路径(你已有 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( - pathNodes, - segmentStart, - segmentEnd, - segmentIndex, - totalPoints, - currPointType, - nextPointType, - isDeadEnd = false, - prevSegmentEndNode = null + + // 根据路径节点(node id 列表)生成最终的经纬路径(含垂足点) + // 说明:pathNodes 包含临时节点 temp_xxx,startConnection/endConnection 帮助在首尾插入垂足 + async generatePathWithPerpendicularConnections( + pathNodes, + segmentStart, + segmentEnd, + startConnection, + endConnection, + graph ) { const segmentPath = []; const segments = []; - - // 生成主要路径段 - for (let j = 0; j < pathNodes.length - 1; j++) { - const currentNode = pathNodes[j]; - const nextNode = pathNodes[j + 1]; - + let hasValidSegment = false; + // 新增:存储途经点垂直点之后的路网后半截(供后续路径延续) + let viaPerpRemainingCoords = []; + + // 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) + segment = this.roadNetworkGeoJSON.features.find(f => + (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 { + } 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; - - segments.push(segment); - const segmentCoords = segment.geometry.coordinates[0]; - - // 处理段内连接 - if (j === 0) { - await this.handleSegmentStartWithReturn( - segmentPath, segmentStart, segmentCoords, segmentIndex, currPointType, prevSegmentEndNode - ); + + if (!segment) { + console.warn(`找不到符合条件的路段: ${curNode} -> ${nextNode}`); + // 若找不到对应路段(可能被避让删掉或数据不一致),则跳过 + continue; + } + + 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]; + } + + // 确定方向:如果 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 { - this.connectSegmentInternally(segmentPath, segmentCoords); + 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); + } + } } } - - // 处理段结束连接(支持死胡同原路返回) - await this.handleSegmentEndWithReturn( - segmentPath, segmentEnd, pathNodes, segmentIndex, totalPoints, nextPointType, isDeadEnd - ); - + } + } + // 如果没有任何有效的路段,返回空路径 + if (!hasValidSegment) { + this.$message.warning('整段路径中没有找到任何符合条件的路段'); + return { path: [], segments: [] }; + } + // 3. 最后加上终点垂足 -> 终点 的直线 + // === 处理终点和途径点的垂足连线 === + // 确保终点/途径点垂足处理 + if (endConnection.type === 'end') { + const endLineCoords = endConnection.lineCoords; + const lastPt = segmentPath[segmentPath.length - 1]; + + // 找到路径末尾点在线段上最近的点索引 + 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) { @@ -2121,7 +2089,7 @@ export default { let nearestIndex = 0; let nearestPoint = coords[0]; let minDistance = this.calculateDistance(point, nearestPoint); - + for (let i = 1; i < coords.length; i++) { const distance = this.calculateDistance(point, coords[i]); if (distance < minDistance) { @@ -2130,7 +2098,7 @@ export default { nearestPoint = coords[i]; } } - + return { index: nearestIndex, point: nearestPoint, distance: minDistance }; }, @@ -2165,30 +2133,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('请先加载路网数据并绘制障碍面、起点和终点!') @@ -2251,23 +2195,6 @@ export default { ) 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 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 @@ + + + + +