Files
bdqh-course-platform-master/src/render/views/CalculationArea/index.vue
2023-11-29 16:08:12 +08:00

531 lines
15 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
<n-grid style="width: 96vw;" class="mx-auto" :cols="2">
<n-gi style="padding-top: 1.5vh; padding-bottom: 0;" class="w-h-full p">
<n-scrollbar style="max-height: 48vh; border: 1px solid #eee">
<n-dynamic-input
v-model:value="geodata"
:on-create="onCreate"
:on-remove="onRemove"
>
<template #default="{ value, index }">
<div style="display: flex; align-items: flex-start; width: 100%">
<div style="width: 50%">
<n-input maxlength="13" clearable v-model:value="value.lat" type="text" placeholder="经度" @input="handleInput" :status="value.latStatus" />
<n-gradient-text :size="12" type="error" v-show="value.latCheck">
<div v-html="value.latErrMsg"></div>
</n-gradient-text>
</div>
<div style="width: 50%">
<n-input maxlength="12" clearable v-model:value="value.lng" type="text" placeholder="纬度" @input="handleInput" :status="value.lngStatus" />
<n-gradient-text :size="12" type="error" v-show="value.lngCheck">
<div v-html="value.lngErrMsg"></div>
</n-gradient-text>
</div>
</div>
<n-button style="padding: 0 5px;font-size: 24px;" @click="move('up', index)">
<n-icon>
<UpIcon />
</n-icon>
</n-button>
<n-button style="padding: 0 5px;font-size: 24px;" @click="move('down', index)">
<n-icon>
<DownIcon />
</n-icon>
</n-button>
</template>
</n-dynamic-input>
</n-scrollbar>
<v-chart style="max-height: 38vh; border: 1px solid #eee" class="chart" id="chart" :option="option" />
<div style="margin-top: 1vh;" class="w-full flex flex-1">
<n-button class="mx-auto" :disabled="geodata.length == 0 ? true : false" size="large" type="primary" @click="calculation">计  算</n-button>
<n-button class="mx-auto" :disabled="geodata.length == 0 ? true : false" size="large" type="error" @click="delgeo">清空输入</n-button>
</div>
</n-gi>
<n-gi style="padding-top: 1.5vh; padding-bottom: 0;" class="w-h-full p">
<n-data-table :columns="columns" :data="data" :single-line="false" :pagination="pagination" :style="{ height: '86vh' }" flex-height />
<div style="margin-top: 1vh;" class="w-full flex flex-1">
<div style="width: 128px" class="mx-auto">
<n-select v-model:value="unit" :options="options" />
</div>
<n-button class="mx-auto" :disabled="data.length == 0 ? true : false" size="large" type="error" @click="delall">清除数据</n-button>
</div>
</n-gi>
</n-grid>
</template>
<script setup lang="ts">
import { ref, h, watch, reactive } from "vue"
import * as echarts from "echarts"
import { use, registerMap } from "echarts/core"
import { CanvasRenderer } from 'echarts/renderers'
import { MapChart, ScatterChart } from 'echarts/charts'
import VChart from "vue-echarts"
import { NGi, NGrid, NButton, NDynamicInput, NDataTable, NScrollbar, NInput, NGradientText, NIcon, NSelect, useMessage, useDialog } from "naive-ui"
import { KeyboardArrowUpRound as UpIcon, KeyboardArrowDownRound as DownIcon } from '@vicons/material'
const message = useMessage()
const dialog = useDialog()
const geodata = ref([
{
lat: "",
latStatus: "",
latCheck: "",
latErrMsg:"",
lng: "",
lngStatus: "",
lngCheck: "",
lngErrMsg:""
}
])
const onCreate = () => {
return {
lat: "",
latStatus: "",
latCheck: "",
latErrMsg:"",
lng: "",
lngStatus: "",
lngCheck: "",
lngErrMsg:""
}
}
const unit = ref('平方米/m²')
const options = [
{
label:"平方米/m²",
value:'平方米/m²'
},
{
label:"平方千米/km²",
value:'平方千米/km²'
}]
const move = (updown, index) => {
let moveit
if(updown == 'up'){
if(index !== 0){
moveit = geodata.value[index]
geodata.value[index] = geodata.value[index - 1]
geodata.value[index - 1] = moveit
moveit = null
}
}
if(updown == 'down'){
if(index !== geodata.value.length-1){
moveit = geodata.value[index]
geodata.value[index] = geodata.value[index + 1]
geodata.value[index + 1] = moveit
moveit = null
}
}
}
const columns = ref([
{
title: '经纬度数据',
key: 'latandlng',
render (row) {
const tags = row.latandlng.map((DataTag) => {
return h(
NScrollbar,
{
style: {
whiteSpace: 'pre-wrap'
}
},
{
default: () => `经度:${DataTag[0]}\n纬度:${DataTag[1]}\n\n`
}
)
})
return tags
}
},
{
title: `面积(单位:${ unit.value })`,
key: 'area'
},
{
title: '轮廓',
key: 'operate',
fixed: 'right',
align: 'center',
render (row) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'large',
type: 'info',
onClick: () => drawContour(row)
},
{ default: () => '生成轮廓' }
)
}
},
{
title: '操作',
key: 'operate',
fixed: 'right',
align: 'center',
render (row) {
return h(
NButton,
{
strong: true,
tertiary: true,
size: 'large',
type: 'error',
onClick: () => deldata(row)
},
{ default: () => '删除' }
)
}
}])
const data = ref([])
const geoObj = reactive({
"type":"FeatureCollection",
"features":[{
"type":"Feature",
"properties":{
"adcode":0,
"name":"经纬度数据",
"center":[0,0],
"centroid":[0,0],
"cp":[15,12],
"childrenNum":0,
"level":"city",
"acroutes":[0,0],
"parent":{"adcode":0}
},
"geometry":{
"type":"MultiPolygon",
"coordinates":[[[[15,30],[0,0],[15,10],[30,0]]]]
}
}]
})
use([ CanvasRenderer, MapChart, ScatterChart ])
registerMap('nowin',geoObj)
const option = reactive({
geo: [
{
zlevel:3,
map: 'nowin',
label: {
show: true,
color: 'black',
fontSize: 18
},
itemStyle: {
normal: {
areaColor: '#26c4eb',
shadowColor:"#01273F",
shadowOffsetX: 0,
shadowOffsetY: 0,
borderWidth:0.5,
}
}
}
],
series: [{
//设置为分散点
type: 'scatter',
//series坐标系类型
coordinateSystem: 'geo',
//设置图形 'circle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow'
symbol: 'pin',
// //标记的大小,可以设置成诸如 10 这样单一的数字,也可以用数组分开表示宽和高,例如 [20, 10] 表示标记宽为20,高为10
symbolSize: [40,40],
//气泡字体设置
label: {
normal: {
show: false,//是否显示
textStyle: {
color: '#000',//字体颜色
fontSize: 8,//字体大小
},
//返回气泡数据
formatter (value){
return value.data.value[2]
}
}
},
itemStyle: {
normal: {
color: '#1E90FF', //标志颜色
areaColor: '#26c4eb',
borderWidth: 0.5,
borderColor: "#ffffff",
shadowColor:"#01273F",
shadowBlur: 6
}
},
// //给区域赋值
data: [],
showEffectOn: 'render',//配置何时显示特效。可选:'render' 绘制完成后显示特效。'emphasis' 高亮(hover)的时候显示特效。
rippleEffect: {//涟漪特效相关配置。
brushType: 'stroke'//波纹的绘制方式,可选 'stroke' 和 'fill'
},
hoverAnimation: true,//是否开启鼠标 hover 的提示动画效果。
zlevel: 3//所属图形的 zlevel 值
}]
})
window.addEventListener('resize', () => {
let chart = echarts.init(document.getElementById('chart'))
chart.resize()
})
const rerender = (mapName,mapArea,mapGeo,initialize) => {
if(mapGeo){
if(initialize){
geoObj.features[0].properties.cp = [15,12]
}
else{
geoObj.features[0].properties.cp = centerPoint(mapGeo)
}
geoObj.features[0].geometry.coordinates[0][0] = mapGeo
}
geoObj.features[0].properties.name = mapArea
registerMap(mapName, geoObj)
option.geo[0].map = mapName
}
const onRemove = () => {
if(geodata.value.length < 3){
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
}
}
const inputFilter = (inputData) => {
let infilter = []
let beffilter = [...inputData]
beffilter.forEach(item => {
if(item !== '\u00A0' && item !== '\u0020' && item !== '\u3000'){
if(!isNaN(Number(item))){
infilter.push(item)
}
else if(item == '.'){
if(infilter.indexOf('.') == '-1'){
infilter.push(item)
}
}
else if(item == '-'){
if(infilter.indexOf('-') == '-1'){
infilter.push(item)
}
}
}
})
return infilter.join('')
}
const stuck = (dataObj,dataItem,Status,Check,ErrMsg,throwMsg) => {
dataObj[dataItem + 'Status'] = Status
dataObj[dataItem + 'Check'] = Check
dataObj[dataItem + 'ErrMsg'] = ErrMsg
if(throwMsg){
message.error(ErrMsg)
throw new Error(ErrMsg)
}
}
const judgment = (dataObj,dataItem,ErrMsg,throwMsg) => {
let toJudgment
if(dataItem == 'lat'){
toJudgment = Number(dataObj[dataItem]).toFixed(8) > 180 || Number(dataObj[dataItem]).toFixed(8) < -180
}
else if(dataItem == "lng"){
toJudgment = Number(dataObj[dataItem]).toFixed(8) > 90 || Number(dataObj[dataItem]).toFixed(8) < -90
}
if(dataObj[dataItem] == ''){
stuck(dataObj,dataItem,'error',true,ErrMsg+'不能为空',throwMsg)
}
else if(dataObj[dataItem].indexOf('-') !== -1 && dataObj[dataItem].indexOf('-') !== 0 || dataObj[dataItem].indexOf('.') == dataObj[dataItem].length-1 || dataObj[dataItem].indexOf('-')+1 == dataObj[dataItem].indexOf('.')){
stuck(dataObj,dataItem,'error',true,ErrMsg+'数据格式错误',throwMsg)
}
else if(toJudgment){
stuck(dataObj,dataItem,'error',true,'非法的'+ErrMsg+'数据',throwMsg)
}
else{
stuck(dataObj,dataItem,'success',false,'',false)
if(throwMsg){
dataObj[dataItem] = Number(dataObj[dataItem]).toString()
}
}
}
const handleInput = () => {
geodata.value.forEach((element,index) => {
element.lat = inputFilter(element.lat)
element.lng = inputFilter(element.lng)
judgment(element,'lat','经度',false)
judgment(element,'lng','纬度',false)
})
}
//地球赤道半径 6378.137
//地球平均半径 6371.004
//弧度转角度 Math.PI / 180.0
//角度转弧度 180.0 / Math.PI
//米转角度 2.0 * Math.PI * 6371.004 / 360.0
//需求决定了输入的经纬度距离不会太远,近似为求平面多边形的面积
const calculationArea = (points) => {
let area = 0
//将多边形分解为三角形
points.forEach((item,index) => {
let j = (index + 1) % points.length
let xi = points[index][0] * (2.0 * Math.PI * 6371004.0 / 360.0) * Math.cos(points[index][1] * (Math.PI / 180.0))
let yi = points[index][1] * (2.0 * Math.PI * 6371004.0 / 360.0)
let xj = points[j][0] * (2.0 * Math.PI * 6371004.0 / 360.0) * Math.cos(points[j][1] * (Math.PI / 180.0))
let yj = points[j][1] * (2.0 * Math.PI * 6371004.0 / 360.0)
area += xi * yj - xj * yi
})
//计算结果为平方米
if(unit.value == '平方米/m²'){
return Math.abs(area / 2).toFixed(4) + unit.value
}
else{
return (Math.abs(area / 2) / 1000000).toFixed(9) + unit.value
}
}
const centerPoint = (allPoint) => {
let averagelat = 0
let averagelng = 0
allPoint.forEach(item => {
averagelat = Number(item[0]) + averagelat
averagelng = Number(item[1]) + averagelng
})
return [averagelat/allPoint.length,averagelng/allPoint.length]
}
const diffData = (areaResult) => {
let allowCalaulation = true
data.value.forEach(items => {
if(items.latandlng.join(';') == areaResult.latandlng.join(';')){
allowCalaulation = false
}
})
if(allowCalaulation){
data.value.push(areaResult)
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
rerender('updataGeo',data.value[data.value.length-1].area,data.value[data.value.length-1].latandlng,false)
}
}
const calculation = () => {
if(geodata.value.length <= 2){
message.error('经纬度数据对的数量不能满足运算')
return
}
let pointArr = []
geodata.value.forEach((element,index) => {
judgment(element,'lat','经度',true)
judgment(element,'lng','纬度',true)
pointArr.push([element.lat.length>8?Number(element.lat).toFixed(8):Number(element.lat), element.lng.length>8?Number(element.lng).toFixed(8):Number(element.lng)])
})
let areaData = {
latandlng: pointArr,
area: calculationArea(pointArr)
}
diffData(areaData)
}
const delgeo = () => {
dialog.warning({
title: '警告',
content: '确定要清除全部输入吗?',
positiveText: '确定',
negativeText: '返回',
onPositiveClick: () => {
geodata.value = [
{
lat: "",
latStatus: "",
latCheck: "",
lng: "",
lngStatus: "",
lngCheck: ""
}
]
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
},
onNegativeClick: () => {
message.error('已返回')
}
})
}
const drawContour = (row) => {
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
rerender('updataGeo',row.area,row.latandlng,false)
}
const deldata = (row) => {
data.value.filter((item,index,array)=>{
if(item == row){
array.splice(index,1)
if(data.value.length == 0){
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
}
}
})
}
const delall = () => {
dialog.warning({
title: '警告',
content: '确定要清除全部数据吗?',
positiveText: '确定',
negativeText: '返回',
onPositiveClick: () => {
data.value = []
rerender('nowin','经纬度数据',[[15,30],[0,0],[15,10],[30,0]],true)
},
onNegativeClick: () => {
message.error('已返回')
}
})
}
watch(unit,(value) => {
columns.value[1].title = `面积(单位:${ value })`
if(data.value.length !== 0){
data.value.forEach(element => {
if(value == '平方米/m²'){
rerender('updataGeo',element.area)
element.area = (element.area.slice(0,element.area.length-8) * 1000000).toFixed(4) + value
rerender('change',element.area)
}else{
rerender('updataGeo',element.area)
element.area = (element.area.slice(0,element.area.length-6) / 1000000).toFixed(9) + value
rerender('change',element.area)
}
})
}
})
</script>
<style scoped lang="less">
.n-grid {
place-items: center
}
.p {
padding: 1%;
padding-top: 2%
}
.mt {
margin-top:2%
}
</style>