12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256 |
- <template>
- <div class="formsWrapper">
- <el-form
- class="form1"
- label-position="left"
- >
- <el-form-item label="场景码">
- <el-input
- v-model="formData.sceneCode"
- autofocus
- />
- </el-form-item>
- <el-button
- type="primary"
- @click="getWholeData"
- >
- 获取、显示数据点
- </el-button>
- <div class="color-desc">
- (按照高度从最低到最高,颜色由黑至红。手动补的点有透明度。)
- </div>
- </el-form>
- <el-form
- class="form2"
- label-position="top"
- >
- <el-form-item
- :label="`路径1数据(必填)(节点index,以英文逗号分隔)(浅蓝色大圆圈表示)(路径长度:${path1Length})`"
- >
- <el-input v-model="formData.path1" />
- </el-form-item>
- <el-form-item
- :label="`路径2数据(选填)(节点index,以英文逗号分隔)(深蓝色小圆圈表示)(路径长度:${path2Length})`"
- >
- <el-input v-model="formData.path2" />
- </el-form-item>
- <el-button
- type="primary"
- @click="renderPath"
- >
- 显示路径
- </el-button>
- <el-button @click="onResetPath">
- 清空
- </el-button>
- </el-form>
- <el-form
- class="form3"
- label-position="top"
- >
- <el-form-item label="起点坐标(以英文逗号分隔)(浅绿色小圆圈表示)">
- <el-input v-model="formData.startPoint" />
- </el-form-item>
- <el-form-item label="终点坐标(以英文逗号分隔)(深绿色小圆圈表示)">
- <el-input v-model="formData.endPoint" />
- </el-form-item>
- <el-button
- type="primary"
- @click="renderStartEndPoint"
- >
- 显示起点终点
- </el-button>
- <el-button @click="onResetStartEndPoint">
- 清空
- </el-button>
- </el-form>
- </div>
- <div class="infoText">
- {{ infoText }}
- </div>
- <div class="map">
- <div class="svgWrapper" />
- <div class="map-control-area">
- <div class="zoom-control panel">
- 聚焦操作
- <el-button
- class="btn"
- type="primary"
- @click="zoomInToStartPoint"
- >
- 聚焦到起点
- </el-button>
- <el-button
- class="btn"
- type="primary"
- @click="zoomInToEndPoint"
- >
- 聚焦到终点
- </el-button>
- <el-button
- class="btn"
- @click="resetZoom"
- >
- 取消聚焦
- </el-button>
- </div>
- <el-form
- class="height-filter panel"
- >
- 高度筛选
- <el-form-item
- :label="`高度上限`"
- >
- <el-input
- v-model="formData.maxHeight"
- type="number"
- step="any"
- />
- </el-form-item>
- <el-form-item
- :label="`高度下限`"
- >
- <el-input
- v-model="formData.minHeight"
- type="number"
- step="any"
- />
- </el-form-item>
- <div class="btn-group">
- <el-button
- type="primary"
- @click="onSetHeightFilter"
- >
- 确定
- </el-button>
- <el-button @click="onResetHeightFilter">
- 清空
- </el-button>
- </div>
- </el-form>
- <el-form
- class="add-point panel"
- >
- 手动补点
- <el-form-item
- :label="`补点模式`"
- >
- <el-switch
- v-model="formData.isAddingPoint"
- />
- </el-form-item>
- <el-form-item
- :label="`高度差上限`"
- title="要想判定两个相邻区域连通,z坐标值之差不得超过此上限"
- >
- <el-input
- v-model="formData.connectionMaxHeightGap"
- type="number"
- step="any"
- />
- </el-form-item>
- <el-form-item
- :label="`补点高度`"
- >
- <el-input
- v-model="formData.addPointHeight"
- type="number"
- step="any"
- />
- </el-form-item>
- <el-button
- type="primary"
- @click="onAddPoint"
- >
- 确定
- </el-button>
- </el-form>
- <div class="edit-button-group">
- <div class="button-row">
- <el-button
- class="btn"
- :disabled="rawWholeDataHistory.currentIdx <= 0"
- @click="onAddPointUndo"
- >
- undo
- </el-button>
- <el-button
- class="btn"
- :disabled="rawWholeDataHistory.currentIdx === rawWholeDataHistory.history.length - 1"
- @click="onAddPointRedo"
- >
- redo
- </el-button>
- </div>
- <div class="button-row">
- <el-button
- class="btn"
- :disabled="rawWholeDataHistory.currentIdx < 0"
- @click="restoreRawWholeData"
- >
- 复原
- </el-button>
- <el-button
- class="btn"
- type="primary"
- :disabled="rawWholeDataHistory.currentIdx < 0"
- @click="uploadAddPointResult"
- >
- 保存
- </el-button>
- </div>
- <!-- <div>{{ rawWholeDataHistory.history.length }}</div>
- <div>{{ rawWholeDataHistory.currentIdx }}</div>
- <div
- v-for="(item, idx) in rawWholeDataHistory.history"
- :key="idx"
- >
- {{ item.length }}
- </div> -->
- </div>
- </div>
- <PointEditor
- v-if="isEditingPoint"
- :initial-point-data="pointDataForEditor"
- @cancel="isEditingPoint = false"
- @confirm="onPointEditorConfirm"
- />
- </div>
- </template>
- <script>
- import * as d3 from "d3";
- import { getWholeData, uploadWholeData, resetWholeData } from "./api.js";
- import { ElLoading, ElMessage } from 'element-plus'
- import {getDistance2D, computePointDistanceAndRowSlope, getNeighbourLocations} from '@/utils.js'
- import deepClone from 'lodash/cloneDeep'
- import PointEditor from '@/components/PointEditor.vue'
- // 视口尺寸
- let svgWidth = document.documentElement.clientWidth - 200
- let svgHeight = document.documentElement.clientHeight - 280
- let svgRatio = svgWidth / svgHeight
- // 全体点位数据
- let rawWholeData = []
- // 用户输入的起点终点
- let startPoint = null
- let endPoint = null
- // 由原始数据算出的几何信息
- let pxPerUnitLength = 0 // 原始数据1单位长度对应的像素数
- let pointDistance = 0 // 最近相邻点间距离(单位:原始数据中长度单位)
- let rowSlope = 0 // 点位构成的排的斜率 [0deg, 90deg)
- let xCenter = 0
- let yCenter = 0
- // svg、d3相关
- let svgNode = null
- let gNode = null
- let zoomObj = null
- let brushObj = null
- let activeRect = null
- let activeRectNeighbourList = []
- // d3选择框位置
- let brushLeftPx = 0
- let brushTopPx = 0
- let brushRightPx = 0
- let brushBottomPx = 0
- function resetGlobalVars() {
- // 视口尺寸
- svgWidth = document.documentElement.clientWidth - 200
- svgHeight = document.documentElement.clientHeight - 280
- svgRatio = svgWidth / svgHeight
- // 全体点位数据
- rawWholeData = []
- // 由原始数据算出的几何信息
- pxPerUnitLength = 0 // 原始数据1单位长度对应的像素数
- pointDistance = 0 // 最近相邻点间距离(单位:原始数据中长度单位)
- rowSlope = 0 // 点位构成的排的斜率 [0deg, 90deg)
- xCenter = 0
- yCenter = 0
- // d3选择框位置
- brushLeftPx = 0
- brushTopPx = 0
- brushRightPx = 0
- brushBottomPx = 0
- }
- function zoomed({transform}) {
- gNode.attr("transform", transform);
- }
- function brushed(e) {
- if (e.selection) {
- console.log('bursh area in px: ', e.selection[0][0], e.selection[0][1], e.selection[1][0], e.selection[1][1]);
- brushLeftPx = e.selection[0][0]
- brushTopPx = e.selection[0][1]
- brushRightPx = e.selection[1][0]
- brushBottomPx = e.selection[1][1]
- } else {
- brushLeftPx = 0
- brushTopPx = 0
- brushRightPx = 0
- brushBottomPx = 0
- }
- }
- export default {
- name: 'App',
- components: {
- PointEditor,
- },
- data() {
- return {
- infoText: '',
- loadingHandler: null,
- formData: {
- // sceneCode: process.env.NODE_ENV === 'production' ? '' : 'SS-t-XkquhxxurM',
- sceneCode: process.env.NODE_ENV === 'production' ? '' : 'SS-t-NZUICC2fRLi',
- path1: process.env.NODE_ENV === 'production' ? '' : '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,47,76,101,125',
- path2: '',
- startPoint: process.env.NODE_ENV === 'production' ? '' : '-38.5, 10.8',
- endPoint: process.env.NODE_ENV === 'production' ? '' : '-28.4, 12.2',
- maxHeight: '',
- minHeight: '',
- isAddingPoint: false,
- connectionMaxHeightGap: 1,
- addPointHeight: 0,
- },
- rawWholeDataHistory: {
- history: [],
- currentIdx: -1,
- },
- isEditingPoint: false,
- pointDataForEditor: {},
- }
- },
- computed: {
- path1Length() {
- return this.getInputPathLength(this.formData.path1)
- },
- path2Length() {
- return this.getInputPathLength(this.formData.path2)
- },
- },
- watch: {
- 'formData.isAddingPoint': {
- handler(vNew) {
- if (vNew) {
- svgNode.on(".zoom", null)
- svgNode.append('g').attr("class", "brush").call(brushObj)
- } else {
- svgNode.call(zoomObj)
- svgNode.selectAll('g.brush').remove()
- }
- }
- }
- },
- mounted() {
- svgNode = d3.select('.svgWrapper').append("svg")
- .attr("width", svgWidth)
- .attr('height', svgHeight)
-
- gNode = svgNode.append('g')
- zoomObj = d3.zoom().on("zoom", zoomed)
- svgNode.call(zoomObj);
- brushObj = d3.brush().on("end", (e) => {
- brushed(e)
- })
-
- svgNode.on('click', function(e, d) {
- activeRect && activeRect.attr('fill', (d) => {
- return activeRect.attr('initial-fill')
- })
- for (const neib of activeRectNeighbourList) {
- neib.attr('fill', (d) => {
- return neib.attr('initial-fill')
- })
- }
- })
- },
- methods: {
- getInputPathLength(input) {
- let temp = input.trim()
- if(temp[0] === '[') { temp = temp.substr(1)}
- if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
- temp = temp.trim()
- if (!temp) {
- return 0
- }
- let mapSuccess = true
- temp = temp.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- const num = Number(item)
- if (!Number.isSafeInteger(num)) {
- mapSuccess = false
- return undefined
- } else {
- return num
- }
- }
- })
- if (!mapSuccess) {
- return '?'
- } else {
- return temp.filter((item) => {
- return item !== undefined
- }).length
- }
- },
- inputPathStringToArray(input) {
- let temp = input.trim()
- if(temp[0] === '[') { temp = temp.substr(1)}
- if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
- temp = temp.trim()
- if (!temp) {
- return []
- }
- let mapSuccess = true
- temp = temp.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- const num = Number(item)
- if (!Number.isSafeInteger(num)) {
- mapSuccess = false
- return undefined
- } else {
- return num
- }
- }
- })
- if (!mapSuccess) {
- window.alert(`解析输入路径失败:${input}`)
- return -1
- } else {
- return temp.filter((item) => {
- return item !== undefined
- })
- }
- },
- getWholeData() {
- if (!this.formData.sceneCode.trim()) {
- window.alert('场景码必填!')
- return
- }
- this.loadingHandler = ElLoading.service({
- lock: true,
- text: 'Loading',
- background: 'rgba(0, 0, 0, 0.7)',
- })
- resetGlobalVars()
- gNode.selectAll('rect').remove()
- gNode.selectAll('text').remove()
- gNode.selectAll('circle').remove()
-
- getWholeData(this.formData.sceneCode).then((res) => {
- this.rawWholeDataHistory.history = []
- this.rawWholeDataHistory.currentIdx = -1
- rawWholeData = res
- this.renderWholePoints()
-
- this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
- this.rawWholeDataHistory.currentIdx++
- }).finally(() => {
- this.loadingHandler.close()
- })
- },
- renderWholePoints() {
- const that = this
- // 相邻点位间距离
- const temp = computePointDistanceAndRowSlope(rawWholeData)
- pointDistance = temp[0]
- rowSlope = temp[1]
-
- // 所有点的分布情况
- let xArray = rawWholeData.map((eachPoint) => {
- return eachPoint.x
- })
- let xLength = Math.max(...xArray) - Math.min(...xArray)
- xCenter = (Math.max(...xArray) + Math.min(...xArray)) / 2
- let yArray = rawWholeData.map((eachPoint) => {
- return eachPoint.y
- })
- let yLength = Math.max(...yArray) - Math.min(...yArray)
- yCenter = (Math.max(...yArray) + Math.min(...yArray)) / 2
- let zArray = rawWholeData.map((eachPoint) => {
- return eachPoint.z
- })
- let zLength = Math.max(...zArray) - Math.min(...zArray)
- let zMin = Math.min(...zArray)
- let areaRatio = xLength / yLength
- // 各个点坐标映射到视口坐标
- if (svgRatio >= areaRatio) { // 分布范围应略小于svg尺寸
- pxPerUnitLength = svgHeight / yLength * 0.9
- } else {
- pxPerUnitLength = svgWidth / xLength * 0.9
- }
- let wholeXArrayInPx = xArray.map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let wholeYArrayInPx = yArray.map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- // 组合成最终数据用来渲染
- let wholeDataForRender = []
- for (let index = 0; index < rawWholeData.length; index++) {
- console.assert(rawWholeData[index].id === (index + 1), '数据点id和数据点在数组中的位置不相符!')
- wholeDataForRender.push([
- wholeXArrayInPx[index],
- wholeYArrayInPx[index],
- zArray[index],
- rawWholeData[index].isManuallyAdded,
- JSON.stringify(rawWholeData[index]),
- rawWholeData[index].id
- ])
- }
- gNode.selectAll('rect').data(wholeDataForRender).enter().append('rect')
- .attr('raw-id', (d) => {
- return d[d.length - 1]
- })
- .attr('x', (d) => d[0] - pointDistance * pxPerUnitLength / 2)
- .attr('y', (d) => d[1] - pointDistance * pxPerUnitLength / 2)
- .attr('width', pointDistance * pxPerUnitLength)
- .attr('height', pointDistance * pxPerUnitLength)
- .attr('fill', (d) => {
- return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, ${d[3] ? '0.7' : '1'})`
- })
- .attr('initial-fill', (d) => {
- return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, ${d[3] ? '0.7' : '1'})`
- })
- .attr('raw-data', (d) => {
- return d[4]
- })
- gNode.selectAll('text').data(wholeDataForRender).enter().append('text')
- .attr('x', (d) => d[0])
- .attr('y', (d) => d[1])
- .attr('font-size', d => pointDistance * pxPerUnitLength / d.length)
- .attr("text-anchor", "middle")
- .attr("dominant-baseline", "middle")
- .attr("fill", "white")
- .attr('pointer-events', 'none')
- .text((d) => {
- return d[d.length - 1]
- })
-
- gNode.selectAll('rect').on('mouseenter', function(e, d) {
- const id = d[d.length - 1]
- if (id !== activeRect?.datum()[activeRect?.datum().length - 1] && !activeRectNeighbourList?.some((item) => {
- return id === item.datum()[item.datum().length - 1]
- })) {
- d3.select(this).attr('fill', 'orange')
- }
- that.infoText = e.target.attributes['raw-data'].value
- }).on('mouseleave', function (e, d) {
- const id = d[d.length - 1]
- if (id !== activeRect?.datum()[activeRect?.datum().length - 1] && !activeRectNeighbourList?.some((item) => {
- return id === item.datum()[item.datum().length - 1]
- })) {
- d3.select(this).attr('fill', d3.select(this).attr('initial-fill'))
- }
- that.infoText = ''
- })
- gNode.selectAll('rect').on('click', function(e, d) {
- e.stopPropagation()
- // console.log(d)
- const id = d[d.length - 1]
- activeRect && activeRect.attr('fill', (d) => {
- return activeRect.attr('initial-fill')
- })
- for (const neib of activeRectNeighbourList) {
- neib.attr('fill', (d) => {
- return neib.attr('initial-fill')
- })
- }
- activeRect = d3.select(this)
- activeRect.attr('fill', d => {
- return 'rgba(0, 0, 255, 1)'
- })
- activeRectNeighbourList = []
- for (const neibId of rawWholeData[id - 1].ids) {
- if (neibId === '-1') {
- continue
- }
- const neibNode = gNode.select(`rect[raw-id='${neibId}']`)
- neibNode.attr('fill', (d) => {
- return 'rgba(0, 0, 128, 1)'
- })
- activeRectNeighbourList.push(neibNode)
- }
- })
- gNode.selectAll('rect').on('dblclick', function(e, d) {
- // console.log(d)
- const id = d[d.length - 1]
- console.log(rawWholeData[id - 1])
- that.isEditingPoint = true
- that.pointDataForEditor = deepClone(rawWholeData[id - 1])
- e.stopPropagation()
- })
- },
- renderPath() {
- if (!this.formData.path1.trim()) {
- window.alert('路径1必填!')
- return
- }
-
- gNode.selectAll('circle.path1').remove()
- gNode.selectAll('circle.path2').remove()
-
- let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
- if (rawPathDataIndex === -1) {
- return
- }
- let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
- if (rawPathDataIndex2 === -1) {
- return
- }
- // 基于path index 拿到path节点数组
- let rawPathData = []
- for (let index = 0; index < rawPathDataIndex.length; index++) {
- const element = rawWholeData[rawPathDataIndex[index] - 1];
- // 假设节点id和节点在数组中出现顺序相符
- console.assert(element.id === rawPathDataIndex[index], '按照id寻找路径节点失败!')
- rawPathData.push(element)
- }
- let rawPathData2 = []
- for (let index = 0; index < rawPathDataIndex2.length; index++) {
- const element = rawWholeData[rawPathDataIndex2[index] - 1];
- // 假设节点id和节点在数组中出现顺序相符
- console.assert(element.id === rawPathDataIndex2[index], '按照id寻找路径节点失败!')
- rawPathData2.push(element)
- }
- // 各个点坐标映射到视口坐标
- let pathXArrayInPx = rawPathData.map((eachPoint) => {
- return eachPoint.x
- }).map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let pathYArrayInPx = rawPathData.map((eachPoint) => {
- return eachPoint.y
- }).map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- let pathXArrayInPx2 = rawPathData2.map((eachPoint) => {
- return eachPoint.x
- }).map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let pathYArrayInPx2 = rawPathData2.map((eachPoint) => {
- return eachPoint.y
- }).map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- // 组合成最终数据用来渲染
- let pathDataForRender = []
- for (let index = 0; index < rawPathData.length; index++) {
- pathDataForRender.push([pathXArrayInPx[index], pathYArrayInPx[index]])
- }
- let pathDataForRender2 = []
- for (let index = 0; index < rawPathData2.length; index++) {
- pathDataForRender2.push([pathXArrayInPx2[index], pathYArrayInPx2[index]])
- }
- // 进行渲染
- gNode.selectAll('circle.path1').data(pathDataForRender).enter().append('circle')
- .classed('path1', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', pointDistance * pxPerUnitLength / 2)
- .attr('fill', '#000088')
- .attr('pointer-events', 'none')
- gNode.selectAll('circle.path2').data(pathDataForRender2).enter().append('circle')
- .classed('path2', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', pointDistance * pxPerUnitLength / 3)
- .attr('fill', 'blue')
- .attr('pointer-events', 'none')
- },
- onResetPath() {
- this.formData.path1 = ''
- this.formData.path2 = ''
- gNode.selectAll('circle.path1').remove()
- gNode.selectAll('circle.path2').remove()
- },
- renderStartEndPoint() {
- gNode.selectAll('circle.start').remove()
- gNode.selectAll('circle.end').remove()
- startPoint = this.formData.startPoint.trim()
- if(startPoint[0] === '[') { startPoint = startPoint.substr(1)}
- if (startPoint[startPoint.length - 1] === ']') { startPoint = startPoint.substr(0, startPoint.length - 1) }
- startPoint = startPoint.trim()
- startPoint = startPoint.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- return Number(item)
- }
- })
- startPoint = startPoint.filter((item) => {
- return item !== undefined
- })
- if (startPoint.length !== 2) {
- window.alert(`解析起点坐标失败`)
- startPoint = null
- }
- endPoint = this.formData.endPoint.trim()
- if(endPoint[0] === '[') { endPoint = endPoint.substr(1)}
- if (endPoint[endPoint.length - 1] === ']') { endPoint = endPoint.substr(0, endPoint.length - 1) }
- endPoint = endPoint.trim()
- endPoint = endPoint.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- return Number(item)
- }
- })
- endPoint = endPoint.filter((item) => {
- return item !== undefined
- })
- if (endPoint.length !== 2) {
- window.alert(`解析终点坐标失败`)
- endPoint = null
- }
-
- if (startPoint) {
- // 起点坐标映射到视口坐标
- startPoint[0] = (startPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
- startPoint[1] = (startPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
- // 进行渲染
- gNode.selectAll('circle.start').data([startPoint]).enter().append('circle')
- .classed('start', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', pointDistance * pxPerUnitLength / 3.5)
- .attr('fill', 'rgba(50, 255, 50, 1)')
- .attr('pointer-events', 'none')
- }
- if (endPoint) {
- // 终点坐标映射到视口坐标
- endPoint[0] = (endPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
- endPoint[1] = (endPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
- // 进行渲染
- gNode.selectAll('circle.end').data([endPoint]).enter().append('circle')
- .classed('end', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', pointDistance * pxPerUnitLength / 3.5)
- .attr('fill', 'green')
- .attr('pointer-events', 'none')
- }
- },
- onResetStartEndPoint() {
- this.formData.startPoint = ''
- this.formData.endPoint = ''
- gNode.selectAll('circle.start').remove()
- gNode.selectAll('circle.end').remove()
- },
- zoomIn(coordinate) {
- if (!Array.isArray(coordinate) || coordinate.length !== 2) {
- return
- }
- svgNode.transition().duration(1000).call(
- zoomObj.transform,
- d3.zoomIdentity.translate(svgWidth / 2, svgHeight / 2).scale(100 / pxPerUnitLength).translate(- coordinate[0], - coordinate[1])
- );
- },
- zoomInToStartPoint() {
- this.zoomIn(startPoint)
- },
- zoomInToEndPoint() {
- this.zoomIn(endPoint)
- },
- resetZoom() {
- svgNode.transition().duration(1000).call(
- zoomObj.transform,
- d3.zoomIdentity.scale(1)
- )
- },
- onSetHeightFilter() {
- gNode.selectAll('rect').attr('visibility', (d) => {
- if ((this.formData.maxHeight !== '') && (Number(this.formData.maxHeight) < d[2])) {
- return 'hidden'
- } else if ((this.formData.minHeight !== '') && (Number(this.formData.minHeight) > d[2])) {
- return 'hidden'
- } else {
- return 'visible'
- }
- })
- },
- onResetHeightFilter() {
- this.formData.maxHeight = ''
- this.formData.minHeight = ''
- gNode.selectAll('rect').attr('visibility', (d) => {
- return 'visible'
- })
- },
- onAddPointUndo() {
- if (this.rawWholeDataHistory.currentIdx > 0) {
- this.rawWholeDataHistory.currentIdx--
- rawWholeData = deepClone(this.rawWholeDataHistory.history[this.rawWholeDataHistory.currentIdx])
-
- gNode.selectAll('rect').remove()
- gNode.selectAll('text').remove()
- this.renderWholePoints()
- }
- },
- onAddPointRedo() {
- if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
- this.rawWholeDataHistory.currentIdx++
- rawWholeData = deepClone(this.rawWholeDataHistory.history[this.rawWholeDataHistory.currentIdx])
-
- gNode.selectAll('rect').remove()
- this.renderWholePoints()
- }
- },
- onAddPoint() {
- // 解析svg的transform信息
- let translateX = 0
- let translateY = 0
- let scale = 1
- const gNodetransformStr = gNode.attr('transform')
- if (gNodetransformStr) {
- const translateStr = gNodetransformStr.split(' ')[0]
- const translateRegex = /translate\(([^,]+),([^,]+)\)/;
- const translateMatch = translateStr.match(translateRegex);
-
- const scaleStr = gNodetransformStr.split(' ')[1]
- const scaleRegex = /scale\(([^)]+)\)/;
- const scaleMatch = scaleStr.match(scaleRegex);
- translateX = Number(translateMatch[1]);
- translateY = Number(translateMatch[2]);
- scale = Number(scaleMatch[1]);
- }
- console.log('transform info: ', translateX, translateY, scale);
- // 选择框位置复原到svg transfrom前
- const brushLeftPxBeformTransform = (brushLeftPx - translateX) / scale
- const brushTopPxBeformTransform = (brushTopPx - translateY) / scale
- const brushRightPxBeformTransform = (brushRightPx - translateX) / scale
- const brushBottomPxBeformTransform = (brushBottomPx - translateY) / scale
-
- // 选择框位置复原到原始坐标系
- const brushLeft = (brushLeftPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
- const brushTop = (brushTopPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
- const brushRight = (brushRightPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
- const brushBottom = (brushBottomPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
- console.log('brush area in raw coordinate: ', brushLeft, brushTop, brushRight, brushBottom);
- // 计算出选择框的外围影响区域
- const affectionAreaLeft = brushLeft - pointDistance * Math.SQRT2 * 1.25
- const affectionAreaTop = brushTop - pointDistance * Math.SQRT2 * 1.25
- const affectionAreaRight = brushRight + pointDistance * Math.SQRT2 * 1.25
- const affectionAreaBottom = brushBottom + pointDistance * Math.SQRT2 * 1.25
- console.log('affection area in raw coordinate: ', affectionAreaLeft, affectionAreaTop, affectionAreaRight, affectionAreaBottom);
- // 筛选出框选区域可能影响到的所有外围点
- // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
- // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
- const affectedPointList = []
- for (const point of rawWholeData) {
- // 如果z不合格,pass
- if (Math.abs(point.z - Number(this.formData.addPointHeight)) > Number(this.formData.connectionMaxHeightGap)) {
- continue
- }
- // 如果在affection范围外,pass
- if (point.x < affectionAreaLeft || point.x > affectionAreaRight || point.y < affectionAreaTop || point.y > affectionAreaBottom) {
- continue
- }
- // 如果在框选区域内,pass
- if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
- continue
- }
- affectedPointList.push(point)
- }
- // todo: 如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
- console.log('affectedPointList: ', affectedPointList);
- // 筛选出框选区域内所有已存在的点
- // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
- // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
- const pointInBrushList = []
- for (const point of rawWholeData) {
- // 如果z不合格,pass
- if (Math.abs(point.z - Number(this.formData.addPointHeight)) > Number(this.formData.connectionMaxHeightGap)) {
- continue
- }
- // 如果在框选区域内,留下
- if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
- pointInBrushList.push(point)
- }
- }
- if (!pointInBrushList.length) {
- window.alert('请保证:1.框选区域内至少已存在一个点位;2.该点位高度与补点高度之间差距不超过高度差上限。该点位将作为点位生长起点。')
- return
- }
- // 拿到框选区域中已存在点的datasetId(所有点都一样),并计算weight平均值
- const pointDatasetId = pointInBrushList[0].datasetId
- let pointWeightSum = 0
- for (const pointInBrush of pointInBrushList) {
- pointWeightSum += pointInBrush.weight
- }
- const pointWeight = pointWeightSum / pointInBrushList.length
-
- // 开始补点
- let activePointIdx = 0
- let newPointId = rawWholeData[rawWholeData.length - 1].id
- // 位于框选区域内的所有点位构成稀疏图模型,用邻接表表示,依次处理其顶点表中各顶点。
- while(activePointIdx <= pointInBrushList.length - 1) {
- // 拿到当前顶点
- const activePoint = pointInBrushList[activePointIdx]
- console.log('顶点表:', pointInBrushList);
- console.log('当前处理顶点Idx:', activePointIdx);
- console.log('当前处理顶点:', activePoint);
- // 当前顶点有可能是本次补点前就存在的,所以清空其邻居信息
- activePoint.ids = []
- const tempIds1 = [] // 临时记录上下左右邻居
- const tempIds2 = [] // 临时记录斜角邻居
-
- // 拿到当前顶点的8个相邻点位
- const neighbourPosList = getNeighbourLocations(pointInBrushList[activePointIdx], pointDistance, rowSlope)
- // 处理8个相邻点位
- for (const key in neighbourPosList) {
- if (Object.hasOwnProperty.call(neighbourPosList, key)) {
- // 看看这个相邻点位属于上下左右邻居还是斜角邻居。
- let neighbourType = 0
- if (['posRight', 'posBottom', 'posLeft', 'posTop'].includes(key)) {
- neighbourType = 1
- } else {
- neighbourType = 2
- }
-
- const neiPos = neighbourPosList[key];
- // 如果点位在框选区域外
- if (neiPos.x < brushLeft || neiPos.x > brushRight || neiPos.y < brushTop || neiPos.y > brushBottom) {
- // 在外围点位表中找匹配的点
- const matchedPoint = affectedPointList.find((affectedPoint) => {
- return getDistance2D(affectedPoint, neiPos) < pointDistance * 0.1
- })
- // 如果找到了匹配点
- if (matchedPoint) {
- console.log(matchedPoint, 'sdfsdf');
- // 记录自己与其关系。
- if (neighbourType === 1) {
- tempIds1.push(String(matchedPoint.id))
- } else {
- tempIds2.push(String(matchedPoint.id))
- }
- // 酌情修改这个外围点位与自己的关系。
- // 相邻点位数组ids中,前4项表示上下左右邻居,后四项表示斜角邻居。
- let idsStartIdx = -1
- let idsEndIdx = -1
- if (neighbourType === 1) {
- idsStartIdx = 0
- idsEndIdx = 4
- } else {
- idsStartIdx = 4
- idsEndIdx = 8
- }
-
- if (!matchedPoint.ids.slice(idsStartIdx, idsEndIdx).find((id) => {
- return id === String(activePoint.id)
- })) {
- const emptyIdx = matchedPoint.ids.slice(idsStartIdx, idsEndIdx).findIndex((id) => {
- return id === '-1'
- })
- console.assert(emptyIdx !== -1, 'emptyIdx居然不存在?!')
- matchedPoint.ids[idsStartIdx + emptyIdx] = String(activePoint.id)
- }
- }
- // 如果没找到匹配点
- else {
- // 记录自己与其关系。
- if (neighbourType === 1) {
- tempIds1.push('-1')
- } else {
- tempIds2.push('-1')
- }
- }
- }
- // 如果点位在框选区域内
- else {
- // 在顶点表中找匹配的点
- const matchedPoint = pointInBrushList.find((pointInBrush) => {
- return getDistance2D(pointInBrush, neiPos) < pointDistance * 0.3
- })
- // 如果找到了匹配点
- if (matchedPoint) {
- // 记录自己与其关系。
- if (neighbourType === 1) {
- tempIds1.push(String(matchedPoint.id))
- } else {
- tempIds2.push(String(matchedPoint.id))
- }
- }
- // 如果没找到匹配点
- else {
- newPointId++
- // 创建新点位,加入顶点表
- pointInBrushList.push({
- id: newPointId,
- x: neiPos.x,
- y: neiPos.y,
- z: Number(this.formData.addPointHeight),
- datasetId: pointDatasetId,
- weight: pointWeight,
- })
- // 记录自己与其关系
- if (neighbourType === 1) {
- tempIds1.push(String(newPointId))
- } else {
- tempIds2.push(String(newPointId))
- }
- }
- }
- }
- } // end of 处理8个相邻点位
- console.assert(tempIds1.length === 4, 'tempIds1长度咋不是4呢?')
- console.assert(tempIds2.length === 4, 'tempIds1长度咋不是4呢?')
- tempIds1.sort((a, b) => {
- if (a === '-1' && b === '-1') {
- return 0
- } else if (a === '-1') {
- return 1
- } else if (b === '-1') {
- return -1
- } else {
- return 0
- }
- })
- tempIds2.sort((a, b) => {
- if (a === '-1' && b === '-1') {
- return 0
- } else if (a === '-1') {
- return 1
- } else if (b === '-1') {
- return -1
- } else {
- return 0
- }
- })
- activePoint.ids = [...tempIds1, ...tempIds2]
-
- activePointIdx++
- } // end of 依次处理顶点表中各顶点
- // 新补的点加入rawWholeData
- const oldIdMax = rawWholeData.length // 旧有的点的id最大值,据此判断哪些点是新增的
- for (const pointInBrush of pointInBrushList) {
- if (pointInBrush.id > oldIdMax) {
- pointInBrush.isManuallyAdded = true
- rawWholeData.push(pointInBrush)
- }
- }
- // 渲染
- this.renderWholePoints()
- // 存入历史记录
- if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
- this.rawWholeDataHistory.history = this.rawWholeDataHistory.history.slice(0, this.rawWholeDataHistory.currentIdx + 1)
- }
- this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
- this.rawWholeDataHistory.currentIdx++
- }, // end of method addPoint
- restoreRawWholeData() {
- if (!this.formData.sceneCode.trim()) {
- window.alert('场景码必填!')
- return
- }
- this.loadingHandler = ElLoading.service({
- lock: true,
- text: 'Loading',
- background: 'rgba(0, 0, 0, 0.7)',
- })
- resetWholeData(this.formData.sceneCode).then((res) => {
- ElMessage({
- message: res,
- type: 'success',
- })
- this.getWholeData()
- }).catch((err) => {
- ElMessage({
- message: err,
- type: 'error',
- })
- }).finally(() => {
- this.loadingHandler.close()
- })
- },
- uploadAddPointResult() {
- if (!this.formData.sceneCode.trim()) {
- window.alert('场景码必填!')
- return
- }
- this.loadingHandler = ElLoading.service({
- lock: true,
- text: 'Loading',
- background: 'rgba(0, 0, 0, 0.7)',
- })
- uploadWholeData(this.formData.sceneCode, rawWholeData).then((res) => {
- ElMessage({
- message: res,
- type: 'success',
- })
- }).catch((err) => {
- ElMessage({
- message: err,
- type: 'error',
- })
- }).finally(() => {
- this.loadingHandler.close()
- })
- },
- onPointEditorConfirm(data) {
- rawWholeData[data.id - 1].x = data.x
- rawWholeData[data.id - 1].y = data.y
- rawWholeData[data.id - 1].z = data.z
- rawWholeData[data.id - 1].weight = data.weight
- rawWholeData[data.id - 1].ids = data.ids
- // 渲染
- gNode.selectAll('rect').remove()
- gNode.selectAll('text').remove()
- this.renderWholePoints()
- // 存入历史记录
- if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
- this.rawWholeDataHistory.history = this.rawWholeDataHistory.history.slice(0, this.rawWholeDataHistory.currentIdx + 1)
- }
- this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
- this.rawWholeDataHistory.currentIdx++
- this.isEditingPoint = false
- }
- },
- }
- </script>
- <style>
- #app {
- font-family: Avenir, Helvetica, Arial, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- color: #2c3e50;
- }
- .formsWrapper {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .formsWrapper .form1 {
- width: 25vw;
- }
- .formsWrapper .form1 .color-desc{
- margin-top: 10px;
- font-size: 12px;
- }
- .formsWrapper .form2 {
- width: 45vw;
- }
- .formsWrapper .form3 {
- width: 25vw;
- }
- .infoText {
- min-height: 2em;
- }
- .map {
- display: flex;
- align-items: center;
- }
- .map > .map-control-area {
- margin-left: 20px;
- }
- .map > .map-control-area > .panel {
- border: 1px solid black;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- margin-bottom: 10px;
- }
- .map > .map-control-area > .panel > .btn {
- margin: 5px;
- }
- .map > .map-control-area > .panel {
- padding-left: 10px;
- padding-right: 10px;
- padding-bottom: 10px;
- }
- .map > .map-control-area > .panel input{
- width: 80px;
- }
- .map > .map-control-area > .panel > .btn-group {
- display: flex;
- justify-content: space-between;
- }
- .map > .map-control-area > .add-point.panel > .btn-group {
- margin-bottom: 10px;
- }
- .map > .map-control-area > .edit-button-group > .button-row{
- margin-bottom: 10px;
- display: flex;
- justify-content: space-evenly;
- }
- .map > .map-control-area > .edit-button-group > .button-row > .btn {
- }
- .svgWrapper {
- display: inline-block;
- overflow: hidden;
- background: #eee;
- }
- </style>
|