diff --git a/package.json b/package.json
index d32a75f..2fe97f6 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@babel/preset-env": "^7.28.3",
- "axios": "0.21.0",
+ "axios": "^1.13.2",
"echarts": "^5.4.3",
"element-ui": "2.9.2",
"file-saver": "^2.0.5",
diff --git a/public/config.ini b/public/config.ini
index b55b302..1630e69 100644
--- a/public/config.ini
+++ b/public/config.ini
@@ -1,6 +1,6 @@
[http]
-port=8083
-address=127.0.0.1
+port=8381
+address=192.168.3.35
[title]
msg=道路堪选分析
\ No newline at end of file
diff --git a/public/config/config.js b/public/config/config.js
new file mode 100644
index 0000000..6cab059
--- /dev/null
+++ b/public/config/config.js
@@ -0,0 +1,4 @@
+window.config = {
+ // baseUrl: 'http://192.168.3.35:8381',
+ baseUrl: '/api'
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 0aed2da..fca558a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -11,6 +11,7 @@
rel="stylesheet"
type="text/css"
/>
+
diff --git a/src/main.js b/src/main.js
index 9780c88..0871257 100644
--- a/src/main.js
+++ b/src/main.js
@@ -10,6 +10,9 @@ import 'vxe-pc-ui/es/style.css'
import VxeUITable from 'vxe-table'
import 'vxe-table/es/style.css'
+import axios from 'axios'
+
+Vue.prototype.$http = axios
Vue.config.productionTip = false
diff --git a/src/utils/map.js b/src/utils/map.js
new file mode 100644
index 0000000..ba6379c
--- /dev/null
+++ b/src/utils/map.js
@@ -0,0 +1,162 @@
+/**
+ * 极简 WKT -> GeoJSON
+ * 支持 POINT / LINESTRING / POLYGON
+ * @param {String} wkt
+ * @return {Object} GeoJSON
+ */
+function wktToGeoJSON (wkt) {
+ const trim = str => str.trim()
+ const split = (str, d) => str.split(d).map(trim)
+
+ // 1. 提取类型 + 坐标字符串
+ const [head, coords] = split(wkt, '(')
+ const type = trim(head).toUpperCase()
+ const coordStr = coords.replace(/\)$/, '')
+
+ // 2. 通用“数字对”解析
+ const parseRing = ring =>
+ split(ring, ',').map(p => {
+ const [x, y] = split(p, ' ').map(Number)
+ return [x, y] // GeoJSON 里经度在前
+ })
+
+ // 3. 按类型组装
+ switch (type) {
+ case 'POINT': {
+ const [x, y] = split(coordStr, ' ').map(Number)
+ return { type: 'Point', coordinates: [x, y] }
+ }
+ case 'LINESTRING':
+ return { type: 'LineString', coordinates: parseRing(coordStr) }
+ case 'POLYGON': {
+ // 可能带孔,先按“), (”切
+ const rings = coordStr.split(/\s*\),\s*\(/).map(parseRing)
+ return { type: 'Polygon', coordinates: rings }
+ }
+ default:
+ throw new Error('暂不支持该 WKT 类型:' + type)
+ }
+}
+
+/* 拿当前视野四至(容错版) */
+function getBounds(map) {
+ let rect = map.camera.computeViewRectangle()
+
+ if (!rect) {
+ // 高空/2D 模式下 computeViewRectangle 会失效,用四个角点兜底
+ const canvas = map.scene.canvas
+ const ellipsoid = Cesium.Ellipsoid.WGS84
+ const cv = (x, y) => map.camera.pickEllipsoid(
+ new Cesium.Cartesian2(x, y), ellipsoid
+ )
+
+ const ptNW = cv(0, 0)
+ const ptSE = cv(canvas.clientWidth, canvas.clientHeight)
+
+ // 如果仍 pick 不到,就缩小范围再试(再不行就返回一个默认矩形)
+ if (!ptNW || !ptSE) {
+ rect = map.camera.computeViewRectangle(
+ ellipsoid,
+ Cesium.Math.toRadians(0.1) // 0.1° 容差
+ )
+ if (!rect) {
+ // 终极保底:以相机位置为中心 ±0.1°
+ const c = map.camera.positionCartographic
+ const d = 0.1
+ return {
+ west : Cesium.Math.toDegrees(c.longitude) - d,
+ east : Cesium.Math.toDegrees(c.longitude) + d,
+ south: Cesium.Math.toDegrees(c.latitude) - d,
+ north: Cesium.Math.toDegrees(c.latitude) + d
+ }
+ }
+ } else {
+ // pick 成功
+ const nw = Cesium.Cartographic.fromCartesian(ptNW)
+ const se = Cesium.Cartographic.fromCartesian(ptSE)
+ return {
+ west : Cesium.Math.toDegrees(nw.longitude),
+ north: Cesium.Math.toDegrees(nw.latitude),
+ east : Cesium.Math.toDegrees(se.longitude),
+ south: Cesium.Math.toDegrees(se.latitude)
+ }
+ }
+ }
+
+ // 正常能算到
+ return {
+ west : Cesium.Math.toDegrees(rect.west),
+ south: Cesium.Math.toDegrees(rect.south),
+ east : Cesium.Math.toDegrees(rect.east),
+ north: Cesium.Math.toDegrees(rect.north)
+ }
+}
+
+
+/* 计算当前地图层级(zoom) */
+function getZoom(map) {
+ const height = map.camera.positionCartographic.height
+ // 经验公式:Cesium 高度 -> WebMercator zoom
+ return Math.floor(29.5 - Math.log(height) / Math.LN2)
+}
+
+/* 画布像素边界 */
+function getViewportPxBounds(map) {
+ const cvs = map.scene.canvas
+ return { left: 0, top: 0, right: cvs.clientWidth, bottom: cvs.clientHeight }
+}
+
+/* 像素 -> 经纬度四至 */
+function pxToLatLngBounds(px, map) {
+ const pick = (x, y) => {
+ const cart = map.camera.pickEllipsoid(
+ new Cesium.Cartesian2(x, y),
+ Cesium.Ellipsoid.WGS84
+ )
+ if (!cart) return null
+ const c = Cesium.Cartographic.fromCartesian(cart)
+ return { lng: Cesium.Math.toDegrees(c.longitude), lat: Cesium.Math.toDegrees(c.latitude) }
+ }
+ const nw = pick(px.left, px.top)
+ const se = pick(px.right, px.bottom)
+ // 兜底:高空 pick 不到就用相机矩形
+ if (!nw || !se) {
+ const rect = map.camera.computeViewRectangle() || {}
+ return {
+ west : Cesium.Math.toDegrees(rect.west || 0),
+ south: Cesium.Math.toDegrees(rect.south || 0),
+ east : Cesium.Math.toDegrees(rect.east || 0),
+ north: Cesium.Math.toDegrees(rect.north || 0)
+ }
+ }
+ return { west: nw.lng, north: nw.lat, east: se.lng, south: se.lat }
+}
+
+
+var GRID_SIZE_DEG = 0.05 // 0.05° ≈ 5 km 一格
+var CACHE_MIN = 30 * 60 * 1000 // 30 min
+/* 经纬度 → 格子 key */
+function lonLatToGridKey (lon, lat) {
+ const x = Math.floor(lon / GRID_SIZE_DEG)
+ const y = Math.floor(lat / GRID_SIZE_DEG)
+ return `${x}_${y}`
+}
+
+/* 取格子中心点(方便防抖) */
+function gridKeyToCenter (key) {
+ const [x, y] = key.split('_').map(Number)
+ return {
+ lon: (x + 0.5) * GRID_SIZE_DEG,
+ lat: (y + 0.5) * GRID_SIZE_DEG
+ }
+}
+
+export {
+ wktToGeoJSON,
+ getBounds,
+ getZoom,
+ getViewportPxBounds,
+ pxToLatLngBounds,
+ lonLatToGridKey,
+ gridKeyToCenter,
+}
\ No newline at end of file
diff --git a/src/views/home/home.vue b/src/views/home/home.vue
index b876d95..0736c45 100644
--- a/src/views/home/home.vue
+++ b/src/views/home/home.vue
@@ -18,19 +18,10 @@
清除
- 确定
+ 确定
路线隐蔽规划
点隐蔽规划
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -303,18 +292,6 @@
{{ row.载重吨 }}
-
-
-
- {{ row.水深 }}
-
-
-
-
-
- {{ row.净空高 }}
-
-
编辑
@@ -386,6 +363,15 @@
import Cookies from 'js-cookie'
import axios from 'axios'
import iniParser from 'ini-parser'
+import {
+ wktToGeoJSON,
+ getBounds,
+ getZoom,
+ lonLatToGridKey,
+ gridKeyToCenter
+} from '@/utils/map.js'
+var GRID_SIZE_DEG = 0.05 // 0.05° ≈ 5 km 一格
+var CACHE_MIN = 30 * 60 * 1000 // 30 min
export default {
data() {
@@ -440,10 +426,19 @@ export default {
json: '',
path: ''
},
- jsonLoading: false
+ jsonLoading: false,
+ roadLayer: null,
+ lastBounds: null, // 上一次四至
+ roadLayerPool: new Map(), // key: "x_y" value: {geoJson, ts}
+ lastGridKey : '', // 当前所在格子
+ debounceTimer: null,
+ _lastGraphicLayer: null,
+ roadLayer: null,
+ parsedData: null,
}
},
async mounted() {
+ this.getBaseUrl()
this.viewer = null
await this.getMapOption()
this.$nextTick(async () => {
@@ -454,6 +449,13 @@ export default {
this.destroyMap()
},
methods: {
+ getBaseUrl() {
+ fetch('./config.ini')
+ .then(response => response.text())
+ .then(text => {
+ this.parsedData = iniParser.parse(text);
+ });
+ },
destroyMap() {
this.clear()
},
@@ -468,7 +470,7 @@ export default {
.catch((error) => {})
},
async initMap() {
- this.mapLoading = true
+ // this.mapLoading = true
this.viewer = new window.mars3d.Map(
'map',
{
@@ -501,15 +503,24 @@ export default {
// ],
} || {}
)
+ // 存储路网信息
+ this.roadLayer = new window.mars3d.layer.GraphicLayer()
+ this.viewer.addLayer(this.roadLayer)
+ // 存储绘制内容
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() // 拿到路网数据
+ // 监听视角高度变化 绘制路网数据
+ setTimeout(() => {
+ this.viewer.on('cameraChanged', this.onCameraChange)
+ this.onCameraChange()
+ }, 0)
+ this.loadFactoryGeoJson() // 拿到本地厂房数据
// 添加地图点击事件监听,用于结束绘制
this.viewer.on('rightClick', (event) => {
// 如果正在绘制,点击地图可以结束绘制(除了绘制点)
@@ -571,70 +582,135 @@ export default {
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: [...]}
+ /** ======================= 根据高度拿到路网 绘制路网 ==================== */
+ // 根据高度开关路网
+ onCameraChange(e) {
+ // 防抖
+ clearTimeout(this.debounceTimer)
+ this.debounceTimer = setTimeout(() => {
+ const zoom = getZoom(this.viewer)
+ /* ===== 1. 层级门槛:≥14 才继续 ===== */
+ if (zoom <= 14) {
+ this.clearRoadLayer() // 低于 14 清掉所有线
+ this.lastGridKey = '' // 重置记忆
+ return
+ }
- // 1. 读取本地 jeojson
- const shpBuffer = await fetch('./config/dao.geojson')
- .then((response) => {
- return response.json()
- })
- .then((data) => {
- return data
- })
- .catch((error) => {})
+ /* 当前相机中心经纬度 → 格子编号 */
+ const c = this.viewer.camera.positionCartographic
+ const centerKey = lonLatToGridKey(
+ Cesium.Math.toDegrees(c.longitude),
+ Cesium.Math.toDegrees(c.latitude)
+ )
- // 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'];
+ /* ===== 2. 格子移动门槛 ===== */
+ if (this.lastGridKey === centerKey) return // 没移动,直接跳过
+ const last = this.lastGridKey ? gridKeyToCenter(this.lastGridKey) : null
+ if (last) {
+ // 计算两次格子中心的实际距离
+ const d = Cesium.Cartesian3.distance(
+ Cesium.Cartesian3.fromDegrees(last.lon, last.lat),
+ Cesium.Cartesian3.fromDegrees(centerKey.lon, centerKey.lat)
+ )
+ // 如果移动距离不足半个格子边长,也跳过(避免边缘抖动)
+ if (d < GRID_SIZE_DEG * 111320 / 2) return
+ }
- // 绘制路网线时循环使用颜色
- 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", // 使用当前颜色
- 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)
- }
+ // 记录本次格子编号并真正加载数据
+ this.lastGridKey = centerKey
+ this.loadGridRoadNet(centerKey)
+ }, 300)
},
- // 获取厂房的数据
+
+ /**
+ * 加载指定格子的路网
+ * @param {string} gridKey 格式 "x_y" 的格子编号
+ */
+ async loadGridRoadNet(gridKey) {
+ /* ===== 3. 缓存命中 ===== */
+ // const hit = this.roadLayerPool.get(gridKey)
+ // if (hit && Date.now() - hit.ts < CACHE_MIN) {
+ // // 缓存未过期,直接复用
+ // this.showCachedLayer(hit.geoJson)
+ // return
+ // }
+
+ /* ===== 4. 计算格子四至(经纬度范围) ===== */
+ const [x, y] = gridKey.split('_').map(Number)
+ const bounds = {
+ west: x * GRID_SIZE_DEG,
+ east: (x + 1) * GRID_SIZE_DEG,
+ south: y * GRID_SIZE_DEG,
+ north: (y + 1) * GRID_SIZE_DEG
+ }
+
+ /* ===== 5. 真正发请求 ===== */
+ const res = await this.loadOnlineShapefile(bounds)
+ if (!res || !res.length) return // 无数据直接返回
+
+ /* 清旧线(只清当前图层) */
+ this.roadLayer.clear()
+ this.roadLayer.show = true
+
+ /* ===== 6. 画线 + 缓存 ===== */
+ // const geoJson = []
+ res.forEach(item => {
+ // WKT → GeoJSON 坐标数组
+ const line = wktToGeoJSON(item.geom_wkt)
+ // 生成 mars3d 折线对象
+ const graphicLine = new mars3d.graphic.PolylineEntity({
+ positions: line.coordinates,
+ style: { color: '#FF0000', width: 2, outline: false }
+ })
+ this.roadLayer.addGraphic(graphicLine)
+ // geoJson.push(line) // 保存一份纯坐标,用于缓存
+ })
+
+ // 写缓存:记录坐标与当前时间戳
+ // this.roadLayerPool.set(gridKey, { geoJson, ts: Date.now() })
+ // this.showCachedLayer(geoJson) // 显示当前格子
+ this.mapLoading = false
+ },
+
+ /* 7. 统一清线方法 */
+ clearRoadLayer() {
+ this.roadLayer.clear() // 清空所有 graphic
+ this.roadLayer.show = false // 隐藏图层
+ // this._lastGraphicLayer = null // 重置上一次记录的图层
+ // this.roadLayerPool.clear() // 同时清缓存
+ },
+
+ /**
+ * 显示缓存的格子
+ * 只做显隐切换,不再重新绘制
+ * @param {Array} geoJson 当前格子的线数组(仅用于占位,实际线已画在 this.graphicLayer)
+ */
+ showCachedLayer(geoJson) {
+ // 1. 隐藏旧格子(如果之前画过)
+ if (this._lastGraphicLayer) {
+ this._lastGraphicLayer.show = false
+ }
+ // 2. 当前格子就是 this.graphicLayer,直接显示
+ this.roadLayer.show = true
+ this._lastGraphicLayer = this.roadLayer // 记录当前正在显示的图层
+ },
+ // 在一定层级范围中加载 在线路网数据
+ async loadOnlineShapefile(latLngBounds) {
+ return new Promise((resolve) => {
+ const url = 'http://' + `${this.parsedData.http.address}:${this.parsedData.http.port}/api/tile`
+ this.$http.post(url, {
+ ...latLngBounds,
+ simplifyTolerance: 0,
+ pageSize: 10000,
+ pageNum: 1
+ }).then((res) => {
+ if (res.status === 200 && res.data.code === 200 && res.data.data) {
+ resolve(res.data.data)
+ }
+ })
+ })
+ },
+ /** ========= 获取厂房的数据 ============= */
async loadFactoryGeoJson() {
try {
const shpBuffer = await fetch('./config/factory.geojson')
@@ -650,7 +726,7 @@ export default {
console.error('加载厂房数据失败:', error)
}
},
- // 去绘制点 进行隐蔽规划
+ /** ========去绘制点 进行隐蔽规划 ========= */
pointBuffer() {
if (this.shortestPathList.length > 0 || this.accordFactoryInfo > 0) {
this.$message.warning('请先清空路线以及路线隐蔽规划')
@@ -716,7 +792,7 @@ export default {
},
})
},
- // 获取当前路线的缓冲区
+ /** ======== 获取当前路线的缓冲区 ============= */
hadBuffer () {
if (this.shortestPathList.length == 0) {
this.$message.warning('请先进行路线规划')
@@ -735,59 +811,35 @@ export default {
}
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);
+ // 创建缓冲区的宽度
+ const bufferWidth = this.hideform.radius; // 避让区的宽度(单位:米)
+ // 确保每条线至少有 2 个点
+ if (this.shortestPathList.length < 2) {
+ return;
+ }
+ try {
+ const segments = [];
+ for (let i = 0; i < this.shortestPathList.length - 1; i++) {
+ segments.push([this.shortestPathList[i], this.shortestPathList[i + 1]]);
}
- }
-
+
+ // 每段单独缓冲区
+ const bufferPromises = segments.map(seg => {
+ const line = turf.lineString(seg);
+ let buffered = turf.buffer(line, bufferWidth / 1000, { units: 'kilometers' });
+ buffered = turf.simplify(buffered, { tolerance: 0.0001, highQuality: false });
+ return buffered;
+ });
+
+ // 合并所有缓冲区(可选)
+ let mergedBuffer = bufferPromises[0];
+ for (let i = 1; i < bufferPromises.length; i++) {
+ mergedBuffer = turf.union(mergedBuffer, bufferPromises[i]);
+ }
+ this.bufferLayerList = [mergedBuffer];
+ } catch (error) {
+ console.error("缓冲分析异常:", error);
+ }
// 去筛选在缓存区的厂房
this.checkFactoryInBuffer('line');
}catch (error) {
@@ -803,7 +855,6 @@ export default {
this.$message.warning('请先选择车辆')
return
}
-
// 将缓冲区数据转换为 Turf.js 的 FeatureCollection
// 遍历每个厂房
const factoryGeoJSON = JSON.parse(JSON.stringify(this.factoryGeoJSON))
@@ -900,6 +951,10 @@ export default {
});
// 过滤出有车辆的工厂
const factoriesWithVehicles = this.accordFactoryInfo.filter(factory => factory.vehicles.length > 0);
+ if (!factoriesWithVehicles.length) {
+ this.$message.info('当前缓冲区范围内未找到可用厂房');
+ return;
+ }
this.factoriesWithVehicles = factoriesWithVehicles
await this.showAreaInfoDialog(factoriesWithVehicles);
})
@@ -1079,21 +1134,16 @@ export default {
},
// 保存列表数据
suerCofirm() {
- 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(`保存在${response.data.path}成功`)
- })
- .catch(error => {
- });
- });
+ axios.post(`http://${this.parsedData.http.address}:${this.parsedData.http.port}/api/equpment`, JSON.stringify(this.tableData), {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(response => {
+ this.$message.success(`保存在${response.data.path}成功`)
+ })
+ .catch(error => {
+ })
},
// 导入json文件
triggerFileUpload() {
@@ -1570,682 +1620,216 @@ export default {
},
})
},
- // 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] : {}
- graph[startNode][endNode] = distance
-
- graph[endNode] = 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
-
- // 构建临时 graph
- const tempGraph = JSON.parse(JSON.stringify(graph))
-
- // 删除避让的边
- if (avoidObstacles) {
- for (const [node, edges] of Object.entries(tempGraph)) {
- for (const targetNode of Object.keys(edges)) {
- const line = turf.lineString([nodeCoords[node], nodeCoords[targetNode]]);
- const intersectsObstacle = avoidObstacles.features.some(area => {
- return turf.booleanIntersects(line, area.geometry) || // 线段与面相交
- turf.booleanWithin(line, area.geometry) || // 线段在面内
- turf.booleanContains(area.geometry, line) // 面包含线段(冗余判断,覆盖边界场景)
- }
- );
- if (intersectsObstacle) {
- delete tempGraph[node][targetNode];
- if (tempGraph[targetNode] && tempGraph[targetNode][node]) {
- delete tempGraph[targetNode][node];
- }
- }
- }
- }
- }
- // 按顺序组合点:起点 -> 途经点 -> 终点(保留途经点原始信息,而非仅坐标)
- const points = [
- { type: 'start', coord: startCoord }, // 标记点类型:起点
- ...viaPoints.map(p => ({ type: 'via', coord: p.geometry.coordinates, raw: p })), // 途经点
- { type: 'end', coord: endCoord } // 终点
- ]
-
- const fullPath = []
- const infoList = []
- 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);
-
- 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);
-
- // 查找路径
- const pathNodes = await this.findPathWithPerpendicularConnections(
- extendedGraph,
- startConnection.tempNodeId,
- endConnection.tempNodeId
- );
-
- if (!pathNodes || pathNodes.length === 0) {
- this.$message.warning(`第${i+1}段路径未找到!`)
- continue
- }
- // 生成当前段的路径(处理死胡同原路返回)
- const segmentResult = await this.generatePathWithPerpendicularConnections(
- pathNodes,
- segmentStart,
- segmentEnd,
- startConnection,
- endConnection,
- extendedGraph
- );
-
- if (segmentResult.path.length > 0) {
- // 合并路径段
- this.mergePathSegments(fullPath, segmentResult.path);
- infoList.push(...segmentResult.segments);
- }
- }
-
- return {fullPath, infoList}
- },
- // 合并路径段
- mergePathSegments(fullPath, newSegment) {
- if (!newSegment || newSegment.length === 0) return;
- if (fullPath.length === 0) {
- fullPath.push(...newSegment);
- return;
- }
- 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;
-
- 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;
- }
-
- 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;
- },
-
-
- // 查找包含垂直连接的路径(你已有 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 null;
- }
- },
-
- // 辅助:在一条线(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 { index: nearestIndex, distance: minDist };
- },
-
- // 根据路径节点(node id 列表)生成最终的经纬路径(含垂足点)
- // 说明:pathNodes 包含临时节点 temp_xxx,startConnection/endConnection 帮助在首尾插入垂足
- async generatePathWithPerpendicularConnections(
- pathNodes,
- segmentStart,
- segmentEnd,
- startConnection,
- endConnection,
- graph
- ) {
- const segmentPath = [];
- const segments = [];
- 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 =>
- (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_ == 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) {
- 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 {
- 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];
-
- // 找到路径末尾点在线段上最近的点索引
- 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 };
- },
-
-
- // 段内小段连接
- connectSegmentInternally(segmentPath, segmentCoords) {
- if (segmentPath.length === 0) {
- segmentPath.push(...segmentCoords);
- return;
- }
-
- const lastPoint = segmentPath[segmentPath.length - 1];
-
- // 找到与上一段终点最接近的点
- const nearestPointInfo = this.findNearestPointWithIndex(segmentCoords, lastPoint);
-
- if (nearestPointInfo.index === 0) {
- // 最近点是段首,正向连接
- if (segmentCoords.length > 1) {
- segmentPath.push(...segmentCoords.slice(1));
- }
- } else if (nearestPointInfo.index === segmentCoords.length - 1) {
- // 最近点是段尾,反向连接
- const reversedCoords = [...segmentCoords].reverse();
- if (reversedCoords.length > 1) {
- segmentPath.push(...reversedCoords.slice(1));
- }
- } else {
- // 最近点在中间,需要分割处理
- this.handleMidpointConnection(segmentPath, segmentCoords, nearestPointInfo);
- }
- },
-
- // 找到线上最近的点及其索引
- findNearestPointWithIndex(coords, point) {
- 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) {
- minDistance = distance;
- nearestIndex = i;
- nearestPoint = coords[i];
- }
- }
-
- return { index: nearestIndex, point: nearestPoint, distance: minDistance };
- },
-
- // 处理中间点连接
- handleMidpointConnection(segmentPath, segmentCoords, nearestPointInfo) {
- const { index, point } = nearestPointInfo;
-
- // 添加最近点
- segmentPath.push(point);
-
- // 决定连接方向(选择较短的部分)
- const distanceToStart = this.calculateDistance(point, segmentCoords[0]);
- const distanceToEnd = this.calculateDistance(point, segmentCoords[segmentCoords.length - 1]);
-
- if (distanceToStart <= distanceToEnd) {
- // 向起点方向连接
- if (index > 0) {
- const toStart = segmentCoords.slice(0, index).reverse();
- segmentPath.push(...toStart);
- }
- } else {
- // 向终点方向连接
- if (index < segmentCoords.length - 1) {
- const toEnd = segmentCoords.slice(index + 1);
- segmentPath.push(...toEnd);
- }
- }
- },
// 计算距离(使用更精确的方法)
calculateDistance(coord1, coord2) {
const point1 = turf.point(coord1);
const point2 = turf.point(coord2);
return turf.distance(point1, point2, { units: 'meters' });
},
- async calculateShortestPath() {
- if (!this.pointQD || !this.pointZD || !this.roadNetworkGeoJSON) {
- this.$message.warning('请先加载路网数据并绘制障碍面、起点和终点!')
+ /** =============== 接口去做路线规划 ====================== */
+ async calculateShortestPathApi() {
+ if (!this.pointQD || !this.pointZD) {
+ 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),
- 100, // 半径 米(根据需求调整)
- {
- steps: 64,
- units: 'meters',
- properties: { type: 'avoid_point' } // 给圆添加属性,方便后续调试区分“点转圆”和“原生区域”
- } // 显式指定单位
- )
- })
- // avoidPointsPolygons.forEach(item => {
- // const polygon = new window.mars3d.graphic.PolygonEntity({
- // positions: item.geometry.coordinates[0],
- // style: {
- // color: '#d4e5db',
- // opacity: 0.5,
- // outline: true,
- // outlineWidth: 1,
- // outlineColor: '#ffffff',
- // },
- // time: new Date().getTime()
- // })
- // this.accordFactoryLayer.addGraphic(polygon);
- // })
- // 避让区域
- const avoidAreasGeoJSON =
- this.avoidAreas.map((area) => turf.feature(area.toGeoJSON({closure: true}.geometry, { type: 'avoid_area' }))) || []
- 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
-
- // 1. 绘制主要路径
- const polyline = new window.mars3d.graphic.PolylinePrimitive({
- positions: positions.fullPath,
- style: {
- clampToGround: true,
- color: '#55ff33',
- width: 8,
- },
- })
- this.shortestPathLayer.addGraphic(polyline)
-
- // 2. 添加方向箭头
- this.addDirectionArrows(positions.fullPath)
-
- this.shortestPathList.push(path.infoList)
- this.infoList = path.infoList.map((item) => item.properties)
- },
+ this.mapLoading = true
+ /* 1. 组装 waypoints:起点 + 途经点 + 终点 */
+ const waypoints = []
+ waypoints.push(this.toLngLat(this.pointQD))
+ this.viaPoints.forEach(p => waypoints.push(this.toLngLat(p)))
+ waypoints.push(this.toLngLat(this.pointZD))
+ /* 2. 组装 avoidPoints */
+ const avoidPoints = this.avoidPoints.map(p => this.toLngLat(p)) || []
+
+ /* 3. 组装 avoidPolygons(支持多圈) */
+ const avoidPolygons = this.avoidAreas.map(graphic => {
+ // 避开区图形转 GeoJSON
+ const coords = graphic.toGeoJSON().geometry.coordinates[0] // 外圈
+ const points = coords.map(c => ({ lng: c[0], lat: c[1] })) || []
+ if (points.length > 0) {
+ points.push(points[0])
+ }
+ return {
+ points: points
+ }
+ })
+
+
+ /* 4. 发请求 */
+ try {
+ const res = await this.loadOnlinePlanning({ waypoints, avoidPoints, avoidPolygons })
+ if (!res || !Array.isArray(res)) {
+ this.$message.warning('后台未返回有效路径!')
+ return
+ }
+ this.drawRouteSegments(res)
+ } catch (e) {
+ // 这里能捕获到「业务异常 / 网络异常 / 超时」
+ this.$message.error('路径规划失败:' + e.message)
+ } finally {
+ this.mapLoading = false
+ }
+ },
+
+ // 获取接口返回的路径规划数据
+ async loadOnlinePlanning (params) {
+ let info = {}
+ if (this.join) {
+ info = {
+ ...params,
+ routeFilterParams: {
+ weightLimit: this.inputform.load,
+ radiusLimit: this.inputform.minTurnRadius,
+ widthLimit: this.inputform.width,
+ enableFilter: true,
+ }
+ }
+ } else {
+ info = {
+ ...params
+ }
+ }
+ return new Promise((resolve, reject) => {
+ // this.$http.post(`/api/api/planning`, info)
+ this.$http.post(`http://${this.parsedData.http.address}:${this.parsedData.http.port}/api/planning`, info)
+ .then(res => {
+ // 业务码正常
+ if (res.status === 200 && res.data) {
+ return resolve(res.data)
+ }
+ // 业务码异常
+ reject(new Error(res.data || '后台返回异常'))
+ })
+ .catch(err => {
+ // 网络 / 500 等
+ reject(new Error('网络错误:' + (err.message || err)))
+ })
+ })
+ },
+ /* ========= 工具:计算最近端并返回是否需要倒序 ========= */
+ getNearestEndpoint(fixedPt, edgeGeomWkt) {
+ const line = wktToGeoJSON(edgeGeomWkt).coordinates.slice() // 深拷贝
+ const first = line[0]
+ const last = line[line.length - 1]
+
+ const dFirst = this.calculateDistance(fixedPt, first)
+ const dLast = this.calculateDistance(fixedPt, last)
+
+ // 返回:{ coord: 最近端坐标, reverse: 是否需要倒序 }
+ return dFirst <= dLast
+ ? { coord: first, reverse: false }
+ : { coord: last, reverse: true }
+ },
+ /* 把 LINESTRING(x1 y1,x2 y2,…) 倒序 */
+ reverseWKTLineString(wkt) {
+ const coords = wkt
+ .slice(11, -1) // 去掉 "LINESTRING(" 和 ")"
+ .split(',')
+ .map(p => p.trim().split(' ').map(Number))
+ coords.reverse()
+ return 'LINESTRING(' + coords.map(p => p.join(' ')).join(',') + ')'
+ },
+ /* 分段绘制接口返回的数组 */
+ drawRouteSegments (segmentArray) {
+ const fullPath = [] // 所有坐标,供后面加箭头
+ const infoList = []
+
+ segmentArray.forEach((seg, idx) => {
+ /* ****** 新增:空路段保护 ****** */
+ if (!seg.edges || seg.edges.length === 0) {
+ this.$message.warning(
+ `未找到可通行路段!`
+ // `第 ${idx + 1} 段路径(${seg.start?.lng},${seg.start?.lat} → ${seg.end?.lng},${seg.end?.lat})未找到可通行路段!`
+ )
+ return // 跳过本段,后续虚线也不补
+ }
+
+ const firstEdge = seg.edges[0]
+ const lastEdge = seg.edges[seg.edges.length - 1]
+
+ /* 6. 补「start→第一条边」虚线 */
+ if (idx === 0) {
+ const startCoord = [seg.start.lng, seg.start.lat]
+ // 判断最近端
+ const { coord: firstCoord, reverse } = this.getNearestEndpoint(
+ startCoord,
+ firstEdge.geomWkt
+ )
+ // 如果需要倒序,把整条边坐标倒过来(后面主流程会用到)
+ if (reverse) {
+ seg.edges[0].geomWkt = this.reverseWKTLineString(firstEdge.geomWkt)
+ }
+ let dashInfo = new mars3d.graphic.PolylinePrimitive({
+ positions: [startCoord, firstCoord],
+ style: { color: '#55ff33', width: 6, opacity: 0.7, lineType: 'dash' }
+ })
+ this.shortestPathLayer.addGraphic(dashInfo)
+ fullPath.push(startCoord, firstCoord)
+ }
+
+ /* 7. 画每一段 edge */
+ seg.edges.forEach(e => {
+ const line = wktToGeoJSON(e.geomWkt).coordinates
+ const poly = new mars3d.graphic.PolylinePrimitive({
+ positions: line,
+ style: { clampToGround: true, color: '#55ff33', width: 8 }
+ })
+ this.shortestPathLayer.addGraphic(poly)
+ /* 在线段中点加一个文字 */
+ // const midIdx = Math.floor(line.length / 2)
+ // const midPos = line[midIdx]
+ // const label = new mars3d.graphic.LabelEntity({
+ // id: 'labe',
+ // position: new mars3d.LngLatPoint(midPos[0], midPos[1]),
+ // style: {
+ // text: e.id.toString(),
+ // font_size: 18,
+ // color: '#55ff33',
+ // outline: true,
+ // outlineColor: '#000000',
+ // pixelOffsetY: -20
+ // }
+ // })
+ // this.shortestPathLayer.addGraphic(label)
+ fullPath.push(...line.slice(1))
+ if (e.id > 0) {
+ infoList.push({ ...e, properties: e })
+ }
+ })
+
+ /* 8. 补「最后一条边→end」虚线 */
+ const endCoord = [seg.end.lng, seg.end.lat]
+ // const lastCoord = wktToGeoJSON(lastEdge.geomWkt).coordinates.slice(-1)[0]
+ const { coord: lastCoord, reverse } = this.getNearestEndpoint(
+ endCoord,
+ lastEdge.geomWkt
+ )
+ // 如果需要倒序,把最后一条边坐标倒过来
+ if (reverse) {
+ seg.edges[seg.edges.length - 1].geomWkt = this.reverseWKTLineString(lastEdge.geomWkt)
+ }
+ const dash = new mars3d.graphic.PolylinePrimitive({
+ positions: [lastCoord, endCoord],
+ style: { color: '#55ff33', width: 6, opacity: 0.7, lineType: 'dash' }
+ })
+ this.shortestPathLayer.addGraphic(dash)
+ fullPath.push(endCoord)
+ })
+
+ /* 9. 加箭头、填表 */
+ // this.addDirectionArrows(fullPath)
+ this.shortestPathList = fullPath
+ this.infoList = infoList.map(i => i.properties || i)
+ },
+ /* 把 mars3d graphic 转 {lng,lat} */
+ toLngLat (graphic) {
+ const c = graphic.toGeoJSON().geometry.coordinates
+ return { lng: c[0], lat: c[1] }
+ },
// 添加方向箭头
addDirectionArrows(pathCoords) {
if (pathCoords.length < 2) return;
@@ -2373,7 +1957,7 @@ export default {
return [tip, left, right, tip] // 闭合三角形
},
- /** 右侧表单导出 */
+ /** ============ 右侧表单导出 =============== */
handleExport() {
if (this.infoList && this.infoList.length == 0) return
const hideData = []
@@ -2392,13 +1976,11 @@ export default {
if (this.infoList.length > 0) {
this.infoList.forEach((item, index) => [
routeData.push({
- 编码: item.编码,
- 名称: item.名称,
- 宽度: item.宽度,
- 曲率半: item.曲率半,
- 载重吨: item.载重吨,
- 水深: item.水深,
- 净空高: item.净空高,
+ 编码: item.num,
+ 名称: item.name,
+ 宽度: item.width,
+ 曲率半: item.radius,
+ 载重吨: item.weight,
id: index
})
])
@@ -2414,8 +1996,6 @@ export default {
宽度: null,
曲率半: null,
载重吨: null,
- 水深: null,
- 净空高: null,
editing: true,
id: this.routeData.length + 1
};
@@ -2484,21 +2064,16 @@ export default {
// 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 => {
- });
- });
+ axios.post(`http://${this.parsedData.http.address}:${this.parsedData.http.port}/api/route`, info, {
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(response => {
+ this.$message.success('导出成功')
+ })
+ .catch(error => {
+ });
},
/** json编辑 */
handleExportJosn() {
@@ -2517,27 +2092,22 @@ export default {
},
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(this.decodeEscapedJson(response.data.json), null, 2)
- }
- this.jsonLoading = false
- })
- .catch(error => {
- this.jsonLoading = false
- });
- });
+ axios.post(`http://${this.parsedData.http.address}:${this.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(this.decodeEscapedJson(response.data.json), null, 2)
+ }
+ this.jsonLoading = false
+ })
+ .catch(error => {
+ this.jsonLoading = false
+ });
},
closeJson() {
this.jsonInfo.path = ''
@@ -2545,23 +2115,17 @@ export default {
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 => {
- });
- });
-
+ axios.post(`http://${this.parsedData.http.address}:${this.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 => {
+ });
},
},
}
diff --git a/src/views/home/home202510115.vue b/src/views/home/home_offlineData.vue
similarity index 89%
rename from src/views/home/home202510115.vue
rename to src/views/home/home_offlineData.vue
index eb72b7d..b876d95 100644
--- a/src/views/home/home202510115.vue
+++ b/src/views/home/home_offlineData.vue
@@ -2,11 +2,21 @@