Files
kxfx/src/views/residentAnalysis/index.vue
yiqiuyang 2b639ea0f3 1
2025-10-23 09:43:28 +08:00

1473 lines
48 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div id="page">
<div class="header">
<div class="images flex a-c" v-for="item in imagesList" :key="item.id">
<img :src="item.src" @click="drawRectangle" />
<el-button class="form-btn" type="primary" size="mini" @click="analyzeAverageSlope">确定</el-button>
<el-button class="form-btn right" type="primary" size="mini" :disabled="showExport" @click="clickExport">
导出
</el-button>
</div>
</div>
<div class="content flex j-s a-c">
<div class="left">
<el-form label-width="140px" label-position="left" size="small">
<!-- 范围 -->
<div class="control-panel">
<div class="title">范围</div>
<el-form-item label="左上角:">
<el-input v-model="form.leftTop" @blur="updateArea" placeholder="请输入经纬度,以 ',' 隔开'"></el-input>
</el-form-item>
<el-form-item label="右下角:">
<el-input v-model="form.rightBottom" @blur="updateArea" placeholder="请输入经纬度,以 ',' 隔开'">
</el-input>
</el-form-item>
<input type="file" ref="fileInput" @change="handleFileUpload" style="display: none" />
<div class="importJson flex j-c a-c" @click="triggerFileUpload">导入json文件</div>
</div>
<!-- 权重 -->
<div class="control-panel">
<div class="title">权重</div>
<el-form-item label="坡度:">
<el-input v-model="form.slopePer"></el-input>
</el-form-item>
<el-form-item label="居民地:">
<el-input v-model="form.peoplePer"></el-input>
</el-form-item>
<el-form-item label="植被:">
<el-input v-model="form.plantPer"></el-input>
</el-form-item>
<el-form-item label="土壤:">
<el-input v-model="form.soilPer"></el-input>
</el-form-item>
</div>
<!-- 条件 -->
<div class="control-panel">
<div class="title">条件</div>
<el-form-item label="长:">
<el-input v-model="form.xLength"></el-input>
</el-form-item>
<el-form-item label="宽:">
<el-input v-model="form.yLength"></el-input>
</el-form-item>
<el-form-item label="最大坡度:">
<el-input v-model="form.maxSlope"></el-input>
</el-form-item>
<el-form-item label="居民地最小距离:">
<el-input v-model="form.minPeopleDis"></el-input>
</el-form-item>
</div>
</el-form>
</div>
<!-- 地图 -->
<div class="center" id="cesiumContainer" v-loading="mapLoading" :element-loading-text="mapLoadText"></div>
<div class="right flex column" v-loading="showLoading" element-loading-text="数据加载中">
<!-- 路点 -->
<div class="table-title" v-if="roadPointList.length > 0">道路附属点</div>
<vxe-table class="item" :data="roadPointList" v-if="roadPointList.length > 0">
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码"></vxe-column>
<vxe-column field="名称" title="名称"></vxe-column>
<vxe-column field="A" title="种类"></vxe-column>
</vxe-table>
<!-- 路线 -->
<div class="table-title" v-if="roadLineList.length > 0">道路附属线</div>
<vxe-table class="item" :data="roadLineList" v-if="roadLineList.length > 0">
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码"></vxe-column>
<vxe-column field="名称" title="名称"></vxe-column>
<vxe-column field="A" title="种类"></vxe-column>
</vxe-table>
<!-- 水点 -->
<div class="table-title" v-if="waterPointList.length > 0">水系附属点</div>
<vxe-table class="item" :data="waterPointList" v-if="waterPointList.length > 0">
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码"></vxe-column>
<vxe-column field="名称" title="名称"></vxe-column>
<vxe-column field="类型" title="类型"></vxe-column>
<vxe-column field="A" title="种类"></vxe-column>
</vxe-table>
<!-- 水线 -->
<div class="table-title" v-if="waterLineList.length > 0">水系附属线</div>
<vxe-table class="item" :data="waterLineList" v-if="waterLineList.length > 0">
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码"></vxe-column>
<vxe-column field="名称" title="名称"></vxe-column>
<vxe-column field="A" title="种类"></vxe-column>
</vxe-table>
</div>
<!-- 导出界面 -->
<el-dialog :visible.sync="dialogVisible" title="导出目录" width="90%" top="10vh">
<el-tabs v-model="activeName" type="card">
<el-tab-pane v-for="(item, index) in tabs" :label="item.label" :name="item.id">
<div class="road flex j-s a-c">
<!-- 道路附属点 -->
<div class="point">
<div class="table-title">
<span>道路附属点</span>
<el-button type="primary" size="mini" @click="handleAdd('roadPointList')">新增</el-button>
</div>
<vxe-table
class="item"
:ref="`${item.id}_roadPointListREF`"
:data="currentTabList.roadPointList"
align="center"
:height="tableHeight"
:row-config="{isCurrent: true, isHover: true, keyField: 'seq'}"
>
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.编码" size="mini"></el-input>
<span v-else>{{ row.编码 }}</span>
</template>
</vxe-column>
<vxe-column field="名称" title="名称">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.名称" size="mini"></el-input>
<span v-else>{{ row.名称 }}</span>
</template>
</vxe-column>
<vxe-column field="A" title="种类">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.A" size="mini"></el-input>
<span v-else>{{ row.A }}</span>
</template>
</vxe-column>
<vxe-column title="操作" width="100">
<template v-slot="{row}">
<el-button v-if="!row.editing" type="text" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button v-else type="text" size="mini" @click="handleSave(row)">保存</el-button>
<el-button type="text" size="mini" @click="handleDelete('roadPointList', row)">删除</el-button>
</template>
</vxe-column>
</vxe-table>
</div>
<!-- 道路附属线 -->
<div class="line">
<div class="table-title">
<span>道路附属线</span>
<el-button type="primary" size="mini" @click="handleAdd('roadLineList')">新增</el-button>
</div>
<vxe-table
class="item"
:ref="`${item.id}_roadLineListREF`"
:data="currentTabList.roadLineList"
align="center"
:height="tableHeight"
:row-config="{isCurrent: true, isHover: true, keyField: 'seq'}"
>
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.编码" size="mini"></el-input>
<span v-else>{{ row.编码 }}</span>
</template>
</vxe-column>
<vxe-column field="名称" title="名称">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.名称" size="mini"></el-input>
<span v-else>{{ row.名称 }}</span>
</template>
</vxe-column>
<vxe-column field="A" title="种类">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.A" size="mini"></el-input>
<span v-else>{{ row.A }}</span>
</template>
</vxe-column>
<vxe-column title="操作" width="100">
<template v-slot="{row}">
<el-button v-if="!row.editing" type="text" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button v-else type="text" size="mini" @click="handleSave(row)">保存</el-button>
<el-button type="text" size="mini" @click="handleDelete('roadLineList', row)">删除</el-button>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
<div class="water flex j-s a-c">
<!-- 水系附属点 -->
<div class="point">
<div class="table-title">
<span>水系附属点</span>
<el-button type="primary" size="mini" @click="handleAdd('waterPointList')">新增</el-button>
</div>
<vxe-table
class="item"
:ref="`${item.id}_waterPointListREF`"
:data="currentTabList.waterPointList"
align="center"
:height="tableHeight"
:row-config="{isCurrent: true, isHover: true, keyField: 'seq'}"
>
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.编码" size="mini"></el-input>
<span v-else>{{ row.编码 }}</span>
</template>
</vxe-column>
<vxe-column field="名称" title="名称">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.名称" size="mini"></el-input>
<span v-else>{{ row.名称 }}</span>
</template>
</vxe-column>
<vxe-column field="A" title="种类">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.A" size="mini"></el-input>
<span v-else>{{ row.A }}</span>
</template>
</vxe-column>
<vxe-column title="操作" width="100">
<template v-slot="{row}">
<el-button v-if="!row.editing" type="text" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button v-else type="text" size="mini" @click="handleSave(row)">保存</el-button>
<el-button type="text" size="mini" @click="handleDelete('waterPointList', row)">删除</el-button>
</template>
</vxe-column>
</vxe-table>
</div>
<!-- 水系附属线 -->
<div class="line">
<div class="table-title">
<span>水系附属线</span>
<el-button type="primary" size="mini" @click="handleAdd('waterLineList')">新增</el-button>
</div>
<vxe-table
class="item"
:ref="`${item.id}_waterLineListREF`"
:data="currentTabList.waterLineList"
align="center"
:height="tableHeight"
:row-config="{isCurrent: true, isHover: true, keyField: 'seq'}"
>
<vxe-column type="seq" width="70"></vxe-column>
<vxe-column field="编码" title="编码">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.编码" size="mini"></el-input>
<span v-else>{{ row.编码 }}</span>
</template>
</vxe-column>
<vxe-column field="名称" title="名称">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.名称" size="mini"></el-input>
<span v-else>{{ row.名称 }}</span>
</template>
</vxe-column>
<vxe-column field="A" title="种类">
<template v-slot="{row}">
<el-input v-if="row.editing" v-model="row.A" size="mini"></el-input>
<span v-else>{{ row.A }}</span>
</template>
</vxe-column>
<vxe-column title="操作" width="100">
<template v-slot="{row}">
<el-button v-if="!row.editing" type="text" size="mini" @click="handleEdit(row)">编辑</el-button>
<el-button v-else type="text" size="mini" @click="handleSave(row)">保存</el-button>
<el-button type="text" size="mini" @click="handleDelete('waterLineList', row)">删除</el-button>
</template>
</vxe-column>
</vxe-table>
</div>
</div>
</el-tab-pane>
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button size="mini" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="mini" @click="exportJSON">导出</el-button>
</div>
</el-dialog>
</div>
</div>
</template>
<script>
import {getStorage} from '@/utils/localStorage'
import plantJson from '/public/config/plant.json'
import soilJson from '/public/config/soil.json'
import FileSaver from 'file-saver'
import axios from 'axios'
import iniParser from 'ini-parser'
export default {
name: '',
data() {
return {
showLoading: false,
imagesList: [{id: 1, src: require('@/assets/image/crop.png')}],
roadPointList: [],
roadLineList: [],
waterPointList: [],
waterLineList: [],
mapLoading: true,
mapLoadText: '地图数据加载中',
form: {
leftTop: '',
rightBottom: '',
slopePer: 0.5,
peoplePer: 0.2,
plantPer: 0.2,
soilPer: 0.1,
xLength: 3,
yLength: 3,
maxSlope: 6,
minPeopleDis: 1,
},
analyzing: false,
rectCount: 0,
isHand: false,
selectedRect: null,
rectangles: [],
labels: [],
peopleGeo: null,
plantGeo: null,
soilGeo: null,
plantJson: null,
soilJson: null,
roadPointGeo: null,
roadLineGeo: null,
waterPointGeo: null,
waterLineGeo: null,
minDistance: null,
_polyCache: null,
validBlocks: [],
colorList: [],
roadWaterLock: false,
dialogVisible: false,
tabs: [],
tabList: [],
rectTotalScore: '',
activeName: '',
tableHeight: 280,
showExport: true,
}
},
created() {
this._polyCache = new WeakMap()
},
mounted() {
this.getMapJson()
this.getColorList()
},
computed: {
// 示例区域:矩形四个角点
positions() {
let a = this.form.leftTop.split(',')
let b = this.form.rightBottom.split(',')
return [
[a[0], b[1]],
[b[0], b[1]],
[b[0], a[1]],
[a[0], a[1]],
]
},
// 导出目录的当前 tab
currentTabList() {
let list = this.tabList.find((item) => item.id === this.activeName)
return list[list?.key]
},
},
beforeDestroy() {
delete window.viewer
delete window.graphicLayer
},
methods: {
// 获取地图配置
getMapJson() {
window.mars3d.Util.fetchJson({
url: `./config/map.json`,
}).then((res) => {
let mapJSonStr = getStorage('localmap')
let mapJson
if (mapJSonStr) {
mapJson = mars3d.Util.merge(res.map3d, mapJSonStr)
} else {
mapJson = res.map3d
}
this.viewerOptions = mapJson
this.plantJson = plantJson
this.soilJson = soilJson
this.initMarsMap()
setTimeout(() => {
this.mapLoading = false
}, 1000)
})
},
// 导入json文件
triggerFileUpload() {
this.$refs.fileInput.click()
},
handleFileUpload(event) {
const file = event.target.files[0]
if (file) {
const reader = new FileReader()
reader.onload = (e) => {
try {
const uploadedJson = JSON.parse(e.target.result)
this.importJson(uploadedJson)
} catch (error) {
console.error('解析 JSON 文件失败:', error)
this.$message.error('解析 JSON 文件失败')
}
}
reader.readAsText(file)
}
},
async importJson(uploadedJson) {
if (!uploadedJson) {
this.$message.warning('请先选择一个 JSON 文件')
return
}
// 清除现有矩形
this.clearRectangles()
if (this.polygon && (uploadedJson?.leftTop || uploadedJson?.rightBottom)) {
window.graphicLayer.removeGraphic(this.polygon)
this.polygon = null
}
this.form.leftTop = uploadedJson.leftTop
this.form.rightBottom = uploadedJson.rightBottom
this.drawInitialArea()
},
getColorList() {
this.colorList = colorMap.createColormap({
colormap: 'jet',
nshades: 10,
format: 'hex',
alpha: 1,
})
},
// 初始化地图
initMarsMap() {
window.viewer = new window.mars3d.Map('cesiumContainer', this.viewerOptions)
window.graphicLayer = new window.mars3d.layer.GraphicLayer()
window.viewer.addLayer(window.graphicLayer)
window.shortestPathLayer = new window.mars3d.layer.GraphicLayer()
window.viewer.addLayer(window.shortestPathLayer)
// 添加地图点击事件监听,用于结束绘制
window.viewer.on(mars3d.EventType.dblClick, (event) => {
// 如果正在绘制,点击地图可以结束绘制(除了绘制点)
window.graphicLayer.stopDraw()
this.form.leftTop = ''
this.form.rightBottom = ''
})
this.loadGeoJson()
},
loadGeoJson() {
// 延迟加载,按需显示
this.layers = {
people: this.createLayerConfig('./config/people.geojson', '#ff0000'),
plant: this.createLayerConfig('./config/plant.geojson', '#00ff00'),
soil: this.createLayerConfig('./config/soil.geojson', '#0000ff'),
roadPoint: this.createLayerConfig('./config/road_points.geojson', '#ff0ff0', 'point'),
roadLine: this.createLayerConfig('./config/road_lines.geojson', '#ff0ff0', 'polyline'),
waterPoint: this.createLayerConfig('./config/water_points.geojson', '#ff0ff0', 'point'),
waterLine: this.createLayerConfig('./config/water_lines.geojson', '#ff0ff0', 'polyline'),
}
// 默认只加载一个,其他按需加载
// this.loadLayers(['people', 'plant', 'soil'], (results) => {
// console.log('所有图层加载完成', results)
// })
this.loadLayer('people')
this.loadLayer('plant', false)
this.loadLayer('soil', false)
},
createLayerConfig(url, color, type = 'polygon') {
const baseConfig = {
layer: null,
config: {
url: url,
crs: 'EPSG:4326',
flyTo: true, // 关闭自动飞行
// 视域裁剪,只显示视野内的要素
cull: {
enabled: true,
cullFace: 'back',
},
// 简化参数
simplify: {
enabled: true,
tolerance: 0.0001,
},
},
}
// 根据不同类型设置不同的符号样式
switch (type) {
case 'point':
baseConfig.config.symbol = {
type: 'point',
styleOptions: {
color: Cesium.Color.fromCssColorString(color),
pixelSize: 8,
outlineColor: Cesium.Color.WHITE,
outlineWidth: 1,
scaleByDistance: new Cesium.NearFarScalar(1.5e2, 1.0, 1.5e6, 0.5),
heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
},
}
break
case 'polyline':
baseConfig.config.symbol = {
type: 'polyline',
styleOptions: {
color: Cesium.Color.fromCssColorString(color),
width: 3,
opacity: 0.8,
clampToGround: true,
},
}
break
case 'polygon':
default:
baseConfig.config.symbol = {
type: 'polygon',
styleOptions: {
color: Cesium.Color.fromCssColorString(color),
opacity: 0.3,
outline: false,
// 如果需要轮廓线可以取消注释
// outlineColor: color,
// outlineWidth: 1,
clampToGround: true, // 贴地显示
},
}
break
}
return baseConfig
},
async loadLayer(layerName, isLoad = true) {
if (this.layers[layerName].layer) {
this.layers[layerName].layer.show = !this.layers[layerName].layer.show
return
}
this.layers[layerName].layer = new mars3d.layer.GeoJsonLayer(this.layers[layerName].config)
isLoad ? await window.viewer.addLayer(this.layers[layerName].layer) : null
},
// 新增方法:同时加载多个图层
async loadLayers(names = [], cb, show = true) {
const tasks = names.map((name) => {
const cfg = this.layers[name]
if (!cfg) return Promise.resolve(null) // 无配置直接过
// 已经加载过:只切换可见性
if (cfg.layer) {
cfg.layer.show = true
return Promise.resolve(cfg.layer)
}
// 首次加载
return new Promise((resolve) => {
const layer = new mars3d.layer.GeoJsonLayer(cfg.config)
layer.once(mars3d.EventType.load, () => resolve(layer))
window.viewer.addLayer(layer)
cfg.layer = layer // 缓存
})
})
return Promise.all(tasks).then((loadedLayers) => {
if (cb) cb(loadedLayers.filter(Boolean)) // 过滤空值
return loadedLayers.filter(Boolean)
})
},
// 算矩形到 geoJSONLayer 的最小距离(米),判断是否在其内部
calcMinDistance(rectPositions, geoJSONLayer) {
if (!geoJSONLayer || !rectPositions?.length) return Infinity
// 1. 矩形边界 + 中心点
const rectLine = turf.lineString(rectPositions.concat([rectPositions[0]]))
const rectCenter = turf.centroid(rectLine)
let minDist = Infinity
let inside = false
const polygons = geoJSONLayer.getGraphics().filter((g) => g.type === 'polygon')
polygons.forEach((g) => {
const lnglats = g.positions.map((p) => {
const carto = Cesium.Cartographic.fromCartesian(p)
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
})
lnglats.push(lnglats[0])
const poly = turf.polygon([lnglats])
if (turf.booleanPointInPolygon(rectCenter, poly)) {
inside = true
return
}
const polyLine = turf.lineString(lnglats)
rectLine.geometry.coordinates.forEach((pt) => {
const closest = turf.nearestPointOnLine(polyLine, pt)
const d = turf.distance(pt, closest, {units: 'metres'})
minDist = Math.min(minDist, d)
})
polyLine.geometry.coordinates.forEach((pt) => {
const closest = turf.nearestPointOnLine(rectLine, pt)
const d = turf.distance(pt, closest, {units: 'metres'})
minDist = Math.min(minDist, d)
})
})
console.log('矩形 → polygonP 边界最短距离(米)', inside, minDist)
// 5. 面内直接返回 -1
return inside ? -1 : minDist
},
// 判断矩形与 mars3d GeoJSON 图层是否相交(不转 GeoJSON
getIntersectId(position, layer) {
console.log('position===>', position, layer)
if (!position || !layer || !layer.graphics) return null
const rectCoords = position.concat([position[0]]) // 闭合
console.log('rectCoords===>', rectCoords)
const rectPoly = turf.polygon([rectCoords])
console.log('rectPoly===>', rectPoly)
const [minX, minY, maxX, maxY] = turf.bbox(rectPoly) // 矩形 bbox
console.log('minX, minY, maxX, maxY===>', minX, minY, maxX, maxY)
let fc = this._polyCache.get(layer)
if (!fc) {
const features = []
layer.graphics.forEach((g) => {
// 处理面要素
if (g.type === 'polygon') {
const coords = g.positions.map((p) => {
const carto = Cesium.Cartographic.fromCartesian(p)
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
})
coords.push(coords[0]) // 闭合
features.push({
type: 'Feature',
geometry: {type: 'Polygon', coordinates: [coords]},
properties: {id: g.attr?.id ?? g.id},
})
}
// 处理线要素
else if (g.type === 'polyline') {
const coords = g.positions.map((p) => {
const carto = Cesium.Cartographic.fromCartesian(p)
return [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
})
features.push({
type: 'Feature',
geometry: {type: 'LineString', coordinates: coords},
properties: {id: g.attr?.id ?? g.id},
})
}
// 处理点要素
else if (g.type === 'point') {
const carto = Cesium.Cartographic.fromCartesian(g.position)
const coord = [Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude)]
features.push({
type: 'Feature',
geometry: {type: 'Point', coordinates: coord},
properties: {id: g.attr?.id ?? g.id},
})
}
})
fc = turf.featureCollection(features)
this._polyCache.set(layer, fc)
}
// 先进行边界框筛选
const candidates = fc.features.filter((f) => {
const [fminX, fminY, fmaxX, fmaxY] = turf.bbox(f)
return !(fmaxX < minX || fminX > maxX || fmaxY < minY || fminY > maxY)
})
// 精确相交检测
for (const f of candidates) {
let isIntersect = false
// 根据几何类型使用不同的相交检测方法
if (f.geometry.type === 'Polygon') {
isIntersect = turf.booleanIntersects(rectPoly, f)
} else if (f.geometry.type === 'LineString') {
// 线与面的相交检测
isIntersect = turf.booleanIntersects(rectPoly, f)
} else if (f.geometry.type === 'Point') {
// 点与面的包含检测
isIntersect = turf.booleanPointInPolygon(f, rectPoly)
}
if (isIntersect) return String(f.properties.id)
}
return null
},
// 手动绘制矩形
async drawRectangle(clampToGround) {
this.clearRectangles()
if (this.polygon) {
window.graphicLayer.removeGraphic(this.polygon)
this.polygon = null
}
this.isHand = true
let isEntityGraphic = false
this.polygon = await window.graphicLayer.startDraw({
type: isEntityGraphic ? 'rectangle' : 'rectangleP',
style: {
color: clampToGround ? '#ffff00' : '#3388ff',
opacity: 0.6,
outline: true,
outlineColor: '#ffffff',
outlineWidth: 2.0,
clampToGround,
},
})
let positions = this.polygon.toJSON().positions
this.form.leftTop = positions[0][0] + ',' + positions[0][1]
this.form.rightBottom = positions[1][0] + ',' + positions[1][1]
},
// 绘制矩形区域
drawInitialArea() {
// 如果多边形尚未创建,则创建它
if (!this.polygon) {
console.log('this.positions===>', this.positions)
this.polygon = new mars3d.graphic.PolygonEntity({
positions: this.positions.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1], 0)),
style: {
color: '#ffff00',
opacity: 0.3,
outline: true,
outlineColor: '#ffffff',
outlineWidth: 2,
},
label: {
text: '分析区域',
font: '16px sans-serif',
color: '#ffffff',
outline: true,
outlineColor: '#000000',
outlineWidth: 2,
pixelOffset: new Cesium.Cartesian2(0, -40),
},
})
window.graphicLayer.addGraphic(this.polygon)
this.isInitialized = true
} else {
// 如果多边形已存在,则更新它的位置
this.updateArea()
}
},
updateArea() {
if (this.isHand) {
this.isHand = false
return
}
if (!this.form.leftTop || !this.form.rightBottom) return
if (this.form.leftTop && this.form.rightBottom && !this.polygon) {
this.drawInitialArea()
}
// 将经纬度转换为笛卡尔坐标
const cartesianPositions = this.positions.map((p) => Cesium.Cartesian3.fromDegrees(p[0], p[1], 0))
// 更新多边形位置
this.polygon.positions = cartesianPositions
// 触发更新根据mars3d的具体API可能需要调用其他方法
if (this.polygon.update) {
this.polygon.update()
}
console.log('区域已更新', this.positions)
},
// 分析平均坡度
async analyzeAverageSlope() {
if (!this.form.leftTop || !this.form.rightBottom) {
return this.$message.warning('请选择范围!')
}
this.mapLoading = true
this.$message.success('开始分析')
this.mapLoadText = '坡度分析中'
this.analyzing = true
this.selectedRect = null
// 清除之前的矩形
this.clearRectangles()
// 获取分割参数
const xSplit = parseInt(this.form.xLength) || 3
const ySplit = parseInt(this.form.yLength) || 3
// 计算区域边界
console.log('this.positions===>', this.positions)
const lats = this.positions.map((p) => p[1])
const lngs = this.positions.map((p) => p[0])
const minLat = Math.min(...lats)
const maxLat = Math.max(...lats)
const minLng = Math.min(...lngs)
const maxLng = Math.max(...lngs)
const latStep = (maxLat - minLat) / ySplit
const lngStep = (maxLng - minLng) / xSplit
// 创建小矩形
this.rectCount = xSplit * ySplit
let rectId = 1
// 收集所有采样点
const samplePoints = []
const rectInfoMap = new Map() // 存储矩形ID对应的信息
for (let i = 0; i < xSplit; i++) {
for (let j = 0; j < ySplit; j++) {
const rectMinLng = minLng + i * lngStep
const rectMaxLng = minLng + (i + 1) * lngStep
const rectMinLat = minLat + j * latStep
const rectMaxLat = minLat + (j + 1) * latStep
// 矩形中心点作为采样点
const centerLng = (rectMinLng + rectMaxLng) / 2
const centerLat = (rectMinLat + rectMaxLat) / 2
samplePoints.push(Cesium.Cartesian3.fromDegrees(centerLng, centerLat))
// 存储矩形信息
rectInfoMap.set(rectId, {
id: rectId,
minLng: rectMinLng,
maxLng: rectMaxLng,
minLat: rectMinLat,
maxLat: rectMaxLat,
center: [centerLng, centerLat],
})
rectId++
}
}
try {
const result = await mars3d.thing.Slope.getSlope({
map: window.viewer,
positions: samplePoints,
splitNum: 1,
radius: 10,
count: 8,
exact: true,
})
console.log('坡度分析结果:', result)
const slopes = result.data.map((d) => d.slope)
for (let i = 0; i < xSplit; i++) {
for (let j = 0; j < ySplit; j++) {
const index = i * ySplit + j
const rectInfo = rectInfoMap.get(index + 1)
const rectPos = [
[rectInfo.minLng, rectInfo.minLat],
[rectInfo.maxLng, rectInfo.minLat],
[rectInfo.maxLng, rectInfo.maxLat],
[rectInfo.minLng, rectInfo.maxLat],
]
const slope = slopes[index]
const distance = this.calcMinDistance(rectPos, this.layers['people'].layer)
if (slope < this.form.maxSlope && distance > this.form.minPeopleDis) {
const soilGeoId = this.getIntersectId(rectPos, this.layers['soil'].layer)
const plantGeoId = this.getIntersectId(rectPos, this.layers['plant'].layer)
const plantScore = plantGeoId
? this.getScore(
this.layers['plant'].layer.getGraphicById(plantGeoId).options.attr['编码'],
this.plantJson
)
: 0
const soilScore = soilGeoId
? this.getScore(this.layers['soil'].layer.getGraphicById(soilGeoId).options.attr['编码'], this.soilJson)
: 1
/* 2. 合并成一条记录 */
this.validBlocks.push({
id: `rect_${index + 1}`,
positions: rectPos,
center: [(rectInfo.minLng + rectInfo.maxLng) / 2, (rectInfo.minLat + rectInfo.maxLat) / 2],
slope,
distance,
plantScore,
soilScore,
index,
})
}
}
}
} catch (e) {
console.error('坡度分析失败', e)
this.$message.error('坡度分析失败,请检查地形数据')
this.mapLoading = false
} finally {
rectInfoMap.clear()
this.drawLabelAndRec()
}
},
drawLabelAndRec() {
this.analyzing = false
/* 1. 计算总分(保持原逻辑) */
const list = this.calcTotalScore(this.validBlocks)
const len = list.length
if (!len) {
this.$message.success('计算结束,暂无符合条件的区域!')
this.mapLoading = false
return
}
/* 2. 一次性构造 options 数组,不 new Graphic */
const rectOpts = []
const labelOpts = []
for (let i = 0; i < len; i++) {
const b = list[i]
const color = this.getColors(list[i].totalScore)
rectOpts.push({
id: b.id,
positions: Cesium.Cartesian3.fromDegreesArray(b.positions.flat()),
style: {
color: color,
opacity: 0.7,
outline: true,
outlineColor: '#ffffff',
outlineWidth: 1,
fill: true,
material: color,
classificationType: Cesium.ClassificationType.BOTH,
},
center: new mars3d.LngLatPoint(b.center[0], b.center[1], 0),
attr: {
isRectangle: true,
originalData: b,
},
depthTestAgainstTerrain: false,
allowPicking: true,
})
let tab = {
id: i + 1 + '',
key: b.totalScore,
[b.totalScore]: {
roadPointList: [],
roadLineList: [],
waterPointList: [],
waterLineList: [],
},
}
this.tabs.push({
id: i + 1 + '',
label: b.totalScore,
})
this.tabList.push(tab)
labelOpts.push({
id: `label_${b.id}`,
position: new mars3d.LngLatPoint(b.center[0], b.center[1], 0),
style: {
text: String(b.totalScore),
font_size: 24,
font_family: '楷体',
color: '#000000',
outline: false,
horizontalOrigin: Cesium.HorizontalOrigin.CENTER,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
visibleDepth: false,
scaleByDistance: true,
disablePicking: true,
},
attr: {
isLabel: true,
relatedRectId: b.id,
},
})
}
this.rectangles = rectOpts.map((opt) => new mars3d.graphic.PolygonEntity(opt))
this.activeName = this.tabs[0].id
console.log('this.rectangles===>', this.rectangles, this.rectangles.length)
this.labels = labelOpts.map((opt) => new mars3d.graphic.LabelEntity(opt))
window.graphicLayer.addGraphic(this.rectangles)
window.graphicLayer.addGraphic(this.labels)
window.graphicLayer.off(mars3d.EventType.click, this._selectRect, this)
this.rectangles.forEach((rectGraphic) => {
rectGraphic.on(mars3d.EventType.click, (event) => {
this._selectRect(event)
})
})
this._selectRect = (event) => {
const graphic = event.graphic
if (graphic && graphic.attr && graphic.attr.isRectangle) {
this.selectRectangle(graphic)
}
}
if (!this.roadWaterLock) {
this.loadLayers(['roadPoint', 'roadLine', 'waterPoint', 'waterLine'], (results) => {
this.roadWaterLock = true
this.$message.success(`计算成功!共有${len}个符合条件的区域`)
this.showExport = false
this.mapLoading = false
})
} else {
this.mapLoading = false
}
},
/**
* 计算综合得分
* @param {Array} list 原始数据
* @returns {Array} 原数组就地添加 totalScore
*/
calcTotalScore(data) {
// 提前计算并缓存范围值
let minSlope = Infinity
let maxSlope = -Infinity
let minDistance = Infinity
let maxDistance = -Infinity
// 单次遍历计算所有范围值
for (let i = 0; i < data.length; i++) {
const item = data[i]
if (item.slope < minSlope) minSlope = item.slope
if (item.slope > maxSlope) maxSlope = item.slope
if (item.distance < minDistance) minDistance = item.distance
if (item.distance > maxDistance) maxDistance = item.distance
}
// 计算常量值,避免重复计算
const slopeRange = maxSlope - minSlope
const distanceRange = maxDistance - minDistance
const isDistanceRangeSmall = distanceRange < 0.0001
// 设置权重
const slopePer = this.form.slopePer
const peoplePer = this.form.peoplePer
const plantPer = this.form.plantPer
const soilPer = this.form.soilPer
const defaultPeoplePer = 0.5 * peoplePer
// 预分配结果数组
const results = new Array(data.length)
// 使用for循环代替map性能更好
for (let i = 0; i < data.length; i++) {
const item = data[i]
// 计算slope分数
const slopeScore = slopeRange > 0 ? ((item.slope - minSlope) / slopeRange) * slopePer : 0.5 * slopePer // 处理slope范围很小的情况
// 计算distance分数
const peopleScore = isDistanceRangeSmall
? defaultPeoplePer
: ((item.distance - minDistance) / distanceRange) * peoplePer
// 计算其他分数
const plantScore = (item.plantScore || 0) * plantPer
const soilScore = item.soilScore * soilPer
const totalScore = (slopeScore + peopleScore + plantScore + soilScore).toFixed(2)
results[i] = {
...item,
slopeScore,
peopleScore,
plantScore,
soilScore,
totalScore,
}
}
return results
},
// 根据坡度值获取颜色
getColors(total) {
const idx = Math.round(Math.max(0, Math.min(9, total * 9)))
return Cesium.Color.fromCssColorString(this.colorList[idx])
},
// 根据 code 查 score
getScore(targetCode, list) {
if (!targetCode) {
return 0
}
const hit = list.find((item) => Array.isArray(item.code) && item.code.includes(targetCode))
if (hit) return hit.score
const fallback = list.find((item) => item.code === null)
return fallback ? fallback.score : 0
},
// 选择矩形
selectRectangle(rect) {
this.showLoading = true
setTimeout(() => {
this.showLoading = false
}, 500)
if (this.selectedRect?.id === rect.id) return
this.selectedRect = {
id: rect.id,
center: rect.center,
}
this.rectTotalScore = rect.attr.originalData.totalScore
this.roadPointList = []
this.roadLineList = []
this.waterPointList = []
this.waterLineList = []
// 清除tablist中原本存在的数据
let tabIndex = this.tabList.findIndex((item) => item.key === this.rectTotalScore)
if (tabIndex !== -1) {
this.tabList[tabIndex][this.rectTotalScore].roadPointList = []
this.tabList[tabIndex][this.rectTotalScore].roadLineList = []
this.tabList[tabIndex][this.rectTotalScore].waterPointList = []
this.tabList[tabIndex][this.rectTotalScore].waterLineList = []
}
this.getRoadWaterIds(rect, tabIndex)
},
// 获取相交的道路和水路
getRoadWaterIds(rect, tabIndex) {
const positions = rect.points.map((d) => [d.lng, d.lat])
let roadPointId = this.getIntersectId(positions, this.layers['roadPoint'].layer)
let roadLineId = this.getIntersectId(positions, this.layers['roadLine'].layer)
let waterPointId = this.getIntersectId(positions, this.layers['waterPoint'].layer)
let waterLineId = this.getIntersectId(positions, this.layers['waterLine'].layer)
if (roadPointId) {
this.roadPointList.push(this.layers['roadPoint'].layer.getGraphicById(roadPointId).options.attr)
this.tabList[tabIndex][this.rectTotalScore].roadPointList.push(
this.layers['roadPoint'].layer.getGraphicById(roadPointId).options.attr
)
} else if (roadLineId) {
this.roadLineList.push(this.layers['roadLine'].layer.getGraphicById(roadLineId).options.attr)
this.tabList[tabIndex][this.rectTotalScore].roadLineList.push(
this.layers['roadLine'].layer.getGraphicById(roadLineId).options.attr
)
} else if (waterPointId) {
this.waterPointList.push(this.layers['waterPoint'].layer.getGraphicById(waterPointId).options.attr)
this.tabList[tabIndex][this.rectTotalScore].waterPointList.push(
this.layers['waterPoint'].layer.getGraphicById(waterPointId).options.attr
)
} else if (waterLineId) {
this.waterLineList.push(this.layers['waterLine'].layer.getGraphicById(waterLineId).options.attr)
this.tabList[tabIndex][this.rectTotalScore].waterLineList.push(
this.layers['waterLine'].layer.getGraphicById(waterLineId).options.attr
)
}
},
// 点击导出按钮
clickExport() {
this.rectangles.forEach((rectGraphic) => {
this.selectRectangle(rectGraphic)
})
this.dialogVisible = true
},
// 导出为 JSON文件
exportJSON() {
// const data = JSON.stringify(this.tabList)
// FileSaver.saveAs(new Blob([data], {type: 'application/json'}), 'data.json')
try {
fetch('./config.ini')
.then((r) => r.text())
.then((res) => {
const parsedData = iniParser.parse(res)
axios
.post(
`http://${parsedData.http.address}:${parsedData.http.port}/api/area`,
JSON.stringify(this.tabList),
{
headers: {'Content-Type': 'application/json'},
}
)
.then((res) => {
this.$message.success(res)
this.$message.success('导出成功!')
})
.catch((error) => {
this.$message.error(error)
})
})
} catch (error) {
console.error(error)
}
},
// 新增
handleAdd(type) {
let tabIndex = this.tabList.findIndex((item) => item.id === this.activeName)
let list = this.tabList[tabIndex]
const newRow = {
seq: list[list.key][type].length + 1,
编码: '',
名称: '',
A: '',
editing: true,
}
list[list.key][type].push(newRow)
setTimeout(() => {
this.$refs[`${this.activeName}_${type}REF`].refreshScroll()
this.$refs[`${this.activeName}_${type}REF`].scrollToRow(newRow, 'id')
}, 50)
},
handleEdit(row) {
this.$set(row, 'editing', true)
},
handleSave(row) {
this.$set(row, 'editing', false)
},
handleDelete(type, row) {
let tabIndex = this.tabList.findIndex((item) => item.id === this.activeName)
let list = this.tabList[tabIndex]
const index = list[list.key][type].findIndex((item) => item.id === row.id)
if (index !== -1) {
list[list.key][type].splice(index, 1)
}
},
// 清除所有矩形
clearRectangles() {
if (this.rectangles.length > 0) {
this.rectangles.forEach((item) => item.remove())
window.graphicLayer.removeGraphic(this.rectangles)
this.rectangles = []
this.rectCount = 0
this.validBlocks = []
}
if (this.labels.length > 0) {
this.labels.forEach((item) => item.remove())
window.graphicLayer.removeGraphic(this.labels)
this.labels = []
}
this.roadPointList = []
this.roadLineList = []
this.waterPointList = []
this.waterLineList = []
this.tabs = []
this.tabList = []
this.rectTotalScore = ''
this.activeName = ''
},
},
}
</script>
<style scoped lang="scss">
::v-deep .el-button--primary {
background-color: #176363;
border-radius: 4px;
&:hover {
background-color: #176363;
}
&:active {
background-color: #176363;
}
}
::v-deep .el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 8px;
}
::v-deep .el-button--primary.is-disabled,
.el-button--primary.is-disabled:active,
.el-button--primary.is-disabled:focus,
.el-button--primary.is-disabled:hover {
background-color: #176363;
}
#page {
width: 100%;
height: 100%;
overflow: hidden;
box-sizing: border-box;
position: relative;
}
.header {
width: 100%;
height: 60px;
background-color: #abc6bc;
position: relative;
.images {
position: relative;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
margin-left: 32px;
}
.form-btn {
margin-left: 30px;
}
.btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 32px;
}
.right {
position: absolute;
right: 30px;
}
}
.content {
width: 100%;
height: calc(100% - 60px);
.left {
width: 348px;
height: 100%;
background-color: #d4e5db;
.control-panel {
width: 340px;
padding: 5px 20px;
box-sizing: border-box;
background-size: cover;
background: #d4e5db;
.title {
font-size: 16px;
margin-bottom: 5px;
color: #1c1c1c;
}
}
.importJson {
background: #2f705f;
border-radius: 4px;
color: #fff;
width: 260px;
height: 24px;
font-size: 14px;
text-align: center;
padding: 3px 0;
}
}
.center {
width: calc(100% - 348px - 452px);
height: 100%;
}
.right {
width: 452px;
height: 100%;
box-sizing: border-box;
background-color: #d4e5db;
padding-left: 20px;
padding-right: 10px;
overflow: auto;
.item {
margin-bottom: 10px;
}
}
}
.table-title {
font-size: 16px;
font-family: 'Pingfang';
font-weight: bold;
letter-spacing: 0.1em;
margin-bottom: 10px;
span {
margin-right: 10px;
}
}
.export {
position: relative;
top: -20px;
}
.road,
.water {
width: 100%;
.point,
.line {
width: 49%;
}
}
.road {
margin-bottom: 20px;
}
</style>