App.vue 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256
  1. <template>
  2. <div class="formsWrapper">
  3. <el-form
  4. class="form1"
  5. label-position="left"
  6. >
  7. <el-form-item label="场景码">
  8. <el-input
  9. v-model="formData.sceneCode"
  10. autofocus
  11. />
  12. </el-form-item>
  13. <el-button
  14. type="primary"
  15. @click="getWholeData"
  16. >
  17. 获取、显示数据点
  18. </el-button>
  19. <div class="color-desc">
  20. (按照高度从最低到最高,颜色由黑至红。手动补的点有透明度。)
  21. </div>
  22. </el-form>
  23. <el-form
  24. class="form2"
  25. label-position="top"
  26. >
  27. <el-form-item
  28. :label="`路径1数据(必填)(节点index,以英文逗号分隔)(浅蓝色大圆圈表示)(路径长度:${path1Length})`"
  29. >
  30. <el-input v-model="formData.path1" />
  31. </el-form-item>
  32. <el-form-item
  33. :label="`路径2数据(选填)(节点index,以英文逗号分隔)(深蓝色小圆圈表示)(路径长度:${path2Length})`"
  34. >
  35. <el-input v-model="formData.path2" />
  36. </el-form-item>
  37. <el-button
  38. type="primary"
  39. @click="renderPath"
  40. >
  41. 显示路径
  42. </el-button>
  43. <el-button @click="onResetPath">
  44. 清空
  45. </el-button>
  46. </el-form>
  47. <el-form
  48. class="form3"
  49. label-position="top"
  50. >
  51. <el-form-item label="起点坐标(以英文逗号分隔)(浅绿色小圆圈表示)">
  52. <el-input v-model="formData.startPoint" />
  53. </el-form-item>
  54. <el-form-item label="终点坐标(以英文逗号分隔)(深绿色小圆圈表示)">
  55. <el-input v-model="formData.endPoint" />
  56. </el-form-item>
  57. <el-button
  58. type="primary"
  59. @click="renderStartEndPoint"
  60. >
  61. 显示起点终点
  62. </el-button>
  63. <el-button @click="onResetStartEndPoint">
  64. 清空
  65. </el-button>
  66. </el-form>
  67. </div>
  68. <div class="infoText">
  69. {{ infoText }}
  70. </div>
  71. <div class="map">
  72. <div class="svgWrapper" />
  73. <div class="map-control-area">
  74. <div class="zoom-control panel">
  75. 聚焦操作
  76. <el-button
  77. class="btn"
  78. type="primary"
  79. @click="zoomInToStartPoint"
  80. >
  81. 聚焦到起点
  82. </el-button>
  83. <el-button
  84. class="btn"
  85. type="primary"
  86. @click="zoomInToEndPoint"
  87. >
  88. 聚焦到终点
  89. </el-button>
  90. <el-button
  91. class="btn"
  92. @click="resetZoom"
  93. >
  94. 取消聚焦
  95. </el-button>
  96. </div>
  97. <el-form
  98. class="height-filter panel"
  99. >
  100. 高度筛选
  101. <el-form-item
  102. :label="`高度上限`"
  103. >
  104. <el-input
  105. v-model="formData.maxHeight"
  106. type="number"
  107. step="any"
  108. />
  109. </el-form-item>
  110. <el-form-item
  111. :label="`高度下限`"
  112. >
  113. <el-input
  114. v-model="formData.minHeight"
  115. type="number"
  116. step="any"
  117. />
  118. </el-form-item>
  119. <div class="btn-group">
  120. <el-button
  121. type="primary"
  122. @click="onSetHeightFilter"
  123. >
  124. 确定
  125. </el-button>
  126. <el-button @click="onResetHeightFilter">
  127. 清空
  128. </el-button>
  129. </div>
  130. </el-form>
  131. <el-form
  132. class="add-point panel"
  133. >
  134. 手动补点
  135. <el-form-item
  136. :label="`补点模式`"
  137. >
  138. <el-switch
  139. v-model="formData.isAddingPoint"
  140. />
  141. </el-form-item>
  142. <el-form-item
  143. :label="`高度差上限`"
  144. title="要想判定两个相邻区域连通,z坐标值之差不得超过此上限"
  145. >
  146. <el-input
  147. v-model="formData.connectionMaxHeightGap"
  148. type="number"
  149. step="any"
  150. />
  151. </el-form-item>
  152. <el-form-item
  153. :label="`补点高度`"
  154. >
  155. <el-input
  156. v-model="formData.addPointHeight"
  157. type="number"
  158. step="any"
  159. />
  160. </el-form-item>
  161. <el-button
  162. type="primary"
  163. @click="onAddPoint"
  164. >
  165. 确定
  166. </el-button>
  167. </el-form>
  168. <div class="edit-button-group">
  169. <div class="button-row">
  170. <el-button
  171. class="btn"
  172. :disabled="rawWholeDataHistory.currentIdx <= 0"
  173. @click="onAddPointUndo"
  174. >
  175. undo
  176. </el-button>
  177. <el-button
  178. class="btn"
  179. :disabled="rawWholeDataHistory.currentIdx === rawWholeDataHistory.history.length - 1"
  180. @click="onAddPointRedo"
  181. >
  182. redo
  183. </el-button>
  184. </div>
  185. <div class="button-row">
  186. <el-button
  187. class="btn"
  188. :disabled="rawWholeDataHistory.currentIdx < 0"
  189. @click="restoreRawWholeData"
  190. >
  191. 复原
  192. </el-button>
  193. <el-button
  194. class="btn"
  195. type="primary"
  196. :disabled="rawWholeDataHistory.currentIdx < 0"
  197. @click="uploadAddPointResult"
  198. >
  199. 保存
  200. </el-button>
  201. </div>
  202. <!-- <div>{{ rawWholeDataHistory.history.length }}</div>
  203. <div>{{ rawWholeDataHistory.currentIdx }}</div>
  204. <div
  205. v-for="(item, idx) in rawWholeDataHistory.history"
  206. :key="idx"
  207. >
  208. {{ item.length }}
  209. </div> -->
  210. </div>
  211. </div>
  212. <PointEditor
  213. v-if="isEditingPoint"
  214. :initial-point-data="pointDataForEditor"
  215. @cancel="isEditingPoint = false"
  216. @confirm="onPointEditorConfirm"
  217. />
  218. </div>
  219. </template>
  220. <script>
  221. import * as d3 from "d3";
  222. import { getWholeData, uploadWholeData, resetWholeData } from "./api.js";
  223. import { ElLoading, ElMessage } from 'element-plus'
  224. import {getDistance2D, computePointDistanceAndRowSlope, getNeighbourLocations} from '@/utils.js'
  225. import deepClone from 'lodash/cloneDeep'
  226. import PointEditor from '@/components/PointEditor.vue'
  227. // 视口尺寸
  228. let svgWidth = document.documentElement.clientWidth - 200
  229. let svgHeight = document.documentElement.clientHeight - 280
  230. let svgRatio = svgWidth / svgHeight
  231. // 全体点位数据
  232. let rawWholeData = []
  233. // 用户输入的起点终点
  234. let startPoint = null
  235. let endPoint = null
  236. // 由原始数据算出的几何信息
  237. let pxPerUnitLength = 0 // 原始数据1单位长度对应的像素数
  238. let pointDistance = 0 // 最近相邻点间距离(单位:原始数据中长度单位)
  239. let rowSlope = 0 // 点位构成的排的斜率 [0deg, 90deg)
  240. let xCenter = 0
  241. let yCenter = 0
  242. // svg、d3相关
  243. let svgNode = null
  244. let gNode = null
  245. let zoomObj = null
  246. let brushObj = null
  247. let activeRect = null
  248. let activeRectNeighbourList = []
  249. // d3选择框位置
  250. let brushLeftPx = 0
  251. let brushTopPx = 0
  252. let brushRightPx = 0
  253. let brushBottomPx = 0
  254. function resetGlobalVars() {
  255. // 视口尺寸
  256. svgWidth = document.documentElement.clientWidth - 200
  257. svgHeight = document.documentElement.clientHeight - 280
  258. svgRatio = svgWidth / svgHeight
  259. // 全体点位数据
  260. rawWholeData = []
  261. // 由原始数据算出的几何信息
  262. pxPerUnitLength = 0 // 原始数据1单位长度对应的像素数
  263. pointDistance = 0 // 最近相邻点间距离(单位:原始数据中长度单位)
  264. rowSlope = 0 // 点位构成的排的斜率 [0deg, 90deg)
  265. xCenter = 0
  266. yCenter = 0
  267. // d3选择框位置
  268. brushLeftPx = 0
  269. brushTopPx = 0
  270. brushRightPx = 0
  271. brushBottomPx = 0
  272. }
  273. function zoomed({transform}) {
  274. gNode.attr("transform", transform);
  275. }
  276. function brushed(e) {
  277. if (e.selection) {
  278. console.log('bursh area in px: ', e.selection[0][0], e.selection[0][1], e.selection[1][0], e.selection[1][1]);
  279. brushLeftPx = e.selection[0][0]
  280. brushTopPx = e.selection[0][1]
  281. brushRightPx = e.selection[1][0]
  282. brushBottomPx = e.selection[1][1]
  283. } else {
  284. brushLeftPx = 0
  285. brushTopPx = 0
  286. brushRightPx = 0
  287. brushBottomPx = 0
  288. }
  289. }
  290. export default {
  291. name: 'App',
  292. components: {
  293. PointEditor,
  294. },
  295. data() {
  296. return {
  297. infoText: '',
  298. loadingHandler: null,
  299. formData: {
  300. // sceneCode: process.env.NODE_ENV === 'production' ? '' : 'SS-t-XkquhxxurM',
  301. sceneCode: process.env.NODE_ENV === 'production' ? '' : 'SS-t-NZUICC2fRLi',
  302. 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',
  303. path2: '',
  304. startPoint: process.env.NODE_ENV === 'production' ? '' : '-38.5, 10.8',
  305. endPoint: process.env.NODE_ENV === 'production' ? '' : '-28.4, 12.2',
  306. maxHeight: '',
  307. minHeight: '',
  308. isAddingPoint: false,
  309. connectionMaxHeightGap: 1,
  310. addPointHeight: 0,
  311. },
  312. rawWholeDataHistory: {
  313. history: [],
  314. currentIdx: -1,
  315. },
  316. isEditingPoint: false,
  317. pointDataForEditor: {},
  318. }
  319. },
  320. computed: {
  321. path1Length() {
  322. return this.getInputPathLength(this.formData.path1)
  323. },
  324. path2Length() {
  325. return this.getInputPathLength(this.formData.path2)
  326. },
  327. },
  328. watch: {
  329. 'formData.isAddingPoint': {
  330. handler(vNew) {
  331. if (vNew) {
  332. svgNode.on(".zoom", null)
  333. svgNode.append('g').attr("class", "brush").call(brushObj)
  334. } else {
  335. svgNode.call(zoomObj)
  336. svgNode.selectAll('g.brush').remove()
  337. }
  338. }
  339. }
  340. },
  341. mounted() {
  342. svgNode = d3.select('.svgWrapper').append("svg")
  343. .attr("width", svgWidth)
  344. .attr('height', svgHeight)
  345. gNode = svgNode.append('g')
  346. zoomObj = d3.zoom().on("zoom", zoomed)
  347. svgNode.call(zoomObj);
  348. brushObj = d3.brush().on("end", (e) => {
  349. brushed(e)
  350. })
  351. svgNode.on('click', function(e, d) {
  352. activeRect && activeRect.attr('fill', (d) => {
  353. return activeRect.attr('initial-fill')
  354. })
  355. for (const neib of activeRectNeighbourList) {
  356. neib.attr('fill', (d) => {
  357. return neib.attr('initial-fill')
  358. })
  359. }
  360. })
  361. },
  362. methods: {
  363. getInputPathLength(input) {
  364. let temp = input.trim()
  365. if(temp[0] === '[') { temp = temp.substr(1)}
  366. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  367. temp = temp.trim()
  368. if (!temp) {
  369. return 0
  370. }
  371. let mapSuccess = true
  372. temp = temp.split(',').map((item) => {
  373. item = item.trim()
  374. if (!item) {
  375. return undefined
  376. } else {
  377. const num = Number(item)
  378. if (!Number.isSafeInteger(num)) {
  379. mapSuccess = false
  380. return undefined
  381. } else {
  382. return num
  383. }
  384. }
  385. })
  386. if (!mapSuccess) {
  387. return '?'
  388. } else {
  389. return temp.filter((item) => {
  390. return item !== undefined
  391. }).length
  392. }
  393. },
  394. inputPathStringToArray(input) {
  395. let temp = input.trim()
  396. if(temp[0] === '[') { temp = temp.substr(1)}
  397. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  398. temp = temp.trim()
  399. if (!temp) {
  400. return []
  401. }
  402. let mapSuccess = true
  403. temp = temp.split(',').map((item) => {
  404. item = item.trim()
  405. if (!item) {
  406. return undefined
  407. } else {
  408. const num = Number(item)
  409. if (!Number.isSafeInteger(num)) {
  410. mapSuccess = false
  411. return undefined
  412. } else {
  413. return num
  414. }
  415. }
  416. })
  417. if (!mapSuccess) {
  418. window.alert(`解析输入路径失败:${input}`)
  419. return -1
  420. } else {
  421. return temp.filter((item) => {
  422. return item !== undefined
  423. })
  424. }
  425. },
  426. getWholeData() {
  427. if (!this.formData.sceneCode.trim()) {
  428. window.alert('场景码必填!')
  429. return
  430. }
  431. this.loadingHandler = ElLoading.service({
  432. lock: true,
  433. text: 'Loading',
  434. background: 'rgba(0, 0, 0, 0.7)',
  435. })
  436. resetGlobalVars()
  437. gNode.selectAll('rect').remove()
  438. gNode.selectAll('text').remove()
  439. gNode.selectAll('circle').remove()
  440. getWholeData(this.formData.sceneCode).then((res) => {
  441. this.rawWholeDataHistory.history = []
  442. this.rawWholeDataHistory.currentIdx = -1
  443. rawWholeData = res
  444. this.renderWholePoints()
  445. this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
  446. this.rawWholeDataHistory.currentIdx++
  447. }).finally(() => {
  448. this.loadingHandler.close()
  449. })
  450. },
  451. renderWholePoints() {
  452. const that = this
  453. // 相邻点位间距离
  454. const temp = computePointDistanceAndRowSlope(rawWholeData)
  455. pointDistance = temp[0]
  456. rowSlope = temp[1]
  457. // 所有点的分布情况
  458. let xArray = rawWholeData.map((eachPoint) => {
  459. return eachPoint.x
  460. })
  461. let xLength = Math.max(...xArray) - Math.min(...xArray)
  462. xCenter = (Math.max(...xArray) + Math.min(...xArray)) / 2
  463. let yArray = rawWholeData.map((eachPoint) => {
  464. return eachPoint.y
  465. })
  466. let yLength = Math.max(...yArray) - Math.min(...yArray)
  467. yCenter = (Math.max(...yArray) + Math.min(...yArray)) / 2
  468. let zArray = rawWholeData.map((eachPoint) => {
  469. return eachPoint.z
  470. })
  471. let zLength = Math.max(...zArray) - Math.min(...zArray)
  472. let zMin = Math.min(...zArray)
  473. let areaRatio = xLength / yLength
  474. // 各个点坐标映射到视口坐标
  475. if (svgRatio >= areaRatio) { // 分布范围应略小于svg尺寸
  476. pxPerUnitLength = svgHeight / yLength * 0.9
  477. } else {
  478. pxPerUnitLength = svgWidth / xLength * 0.9
  479. }
  480. let wholeXArrayInPx = xArray.map((eachX) => {
  481. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  482. })
  483. let wholeYArrayInPx = yArray.map((eachY) => {
  484. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  485. })
  486. // 组合成最终数据用来渲染
  487. let wholeDataForRender = []
  488. for (let index = 0; index < rawWholeData.length; index++) {
  489. console.assert(rawWholeData[index].id === (index + 1), '数据点id和数据点在数组中的位置不相符!')
  490. wholeDataForRender.push([
  491. wholeXArrayInPx[index],
  492. wholeYArrayInPx[index],
  493. zArray[index],
  494. rawWholeData[index].isManuallyAdded,
  495. JSON.stringify(rawWholeData[index]),
  496. rawWholeData[index].id
  497. ])
  498. }
  499. gNode.selectAll('rect').data(wholeDataForRender).enter().append('rect')
  500. .attr('raw-id', (d) => {
  501. return d[d.length - 1]
  502. })
  503. .attr('x', (d) => d[0] - pointDistance * pxPerUnitLength / 2)
  504. .attr('y', (d) => d[1] - pointDistance * pxPerUnitLength / 2)
  505. .attr('width', pointDistance * pxPerUnitLength)
  506. .attr('height', pointDistance * pxPerUnitLength)
  507. .attr('fill', (d) => {
  508. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, ${d[3] ? '0.7' : '1'})`
  509. })
  510. .attr('initial-fill', (d) => {
  511. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, ${d[3] ? '0.7' : '1'})`
  512. })
  513. .attr('raw-data', (d) => {
  514. return d[4]
  515. })
  516. gNode.selectAll('text').data(wholeDataForRender).enter().append('text')
  517. .attr('x', (d) => d[0])
  518. .attr('y', (d) => d[1])
  519. .attr('font-size', d => pointDistance * pxPerUnitLength / d.length)
  520. .attr("text-anchor", "middle")
  521. .attr("dominant-baseline", "middle")
  522. .attr("fill", "white")
  523. .attr('pointer-events', 'none')
  524. .text((d) => {
  525. return d[d.length - 1]
  526. })
  527. gNode.selectAll('rect').on('mouseenter', function(e, d) {
  528. const id = d[d.length - 1]
  529. if (id !== activeRect?.datum()[activeRect?.datum().length - 1] && !activeRectNeighbourList?.some((item) => {
  530. return id === item.datum()[item.datum().length - 1]
  531. })) {
  532. d3.select(this).attr('fill', 'orange')
  533. }
  534. that.infoText = e.target.attributes['raw-data'].value
  535. }).on('mouseleave', function (e, d) {
  536. const id = d[d.length - 1]
  537. if (id !== activeRect?.datum()[activeRect?.datum().length - 1] && !activeRectNeighbourList?.some((item) => {
  538. return id === item.datum()[item.datum().length - 1]
  539. })) {
  540. d3.select(this).attr('fill', d3.select(this).attr('initial-fill'))
  541. }
  542. that.infoText = ''
  543. })
  544. gNode.selectAll('rect').on('click', function(e, d) {
  545. e.stopPropagation()
  546. // console.log(d)
  547. const id = d[d.length - 1]
  548. activeRect && activeRect.attr('fill', (d) => {
  549. return activeRect.attr('initial-fill')
  550. })
  551. for (const neib of activeRectNeighbourList) {
  552. neib.attr('fill', (d) => {
  553. return neib.attr('initial-fill')
  554. })
  555. }
  556. activeRect = d3.select(this)
  557. activeRect.attr('fill', d => {
  558. return 'rgba(0, 0, 255, 1)'
  559. })
  560. activeRectNeighbourList = []
  561. for (const neibId of rawWholeData[id - 1].ids) {
  562. if (neibId === '-1') {
  563. continue
  564. }
  565. const neibNode = gNode.select(`rect[raw-id='${neibId}']`)
  566. neibNode.attr('fill', (d) => {
  567. return 'rgba(0, 0, 128, 1)'
  568. })
  569. activeRectNeighbourList.push(neibNode)
  570. }
  571. })
  572. gNode.selectAll('rect').on('dblclick', function(e, d) {
  573. // console.log(d)
  574. const id = d[d.length - 1]
  575. console.log(rawWholeData[id - 1])
  576. that.isEditingPoint = true
  577. that.pointDataForEditor = deepClone(rawWholeData[id - 1])
  578. e.stopPropagation()
  579. })
  580. },
  581. renderPath() {
  582. if (!this.formData.path1.trim()) {
  583. window.alert('路径1必填!')
  584. return
  585. }
  586. gNode.selectAll('circle.path1').remove()
  587. gNode.selectAll('circle.path2').remove()
  588. let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
  589. if (rawPathDataIndex === -1) {
  590. return
  591. }
  592. let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
  593. if (rawPathDataIndex2 === -1) {
  594. return
  595. }
  596. // 基于path index 拿到path节点数组
  597. let rawPathData = []
  598. for (let index = 0; index < rawPathDataIndex.length; index++) {
  599. const element = rawWholeData[rawPathDataIndex[index] - 1];
  600. // 假设节点id和节点在数组中出现顺序相符
  601. console.assert(element.id === rawPathDataIndex[index], '按照id寻找路径节点失败!')
  602. rawPathData.push(element)
  603. }
  604. let rawPathData2 = []
  605. for (let index = 0; index < rawPathDataIndex2.length; index++) {
  606. const element = rawWholeData[rawPathDataIndex2[index] - 1];
  607. // 假设节点id和节点在数组中出现顺序相符
  608. console.assert(element.id === rawPathDataIndex2[index], '按照id寻找路径节点失败!')
  609. rawPathData2.push(element)
  610. }
  611. // 各个点坐标映射到视口坐标
  612. let pathXArrayInPx = rawPathData.map((eachPoint) => {
  613. return eachPoint.x
  614. }).map((eachX) => {
  615. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  616. })
  617. let pathYArrayInPx = rawPathData.map((eachPoint) => {
  618. return eachPoint.y
  619. }).map((eachY) => {
  620. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  621. })
  622. let pathXArrayInPx2 = rawPathData2.map((eachPoint) => {
  623. return eachPoint.x
  624. }).map((eachX) => {
  625. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  626. })
  627. let pathYArrayInPx2 = rawPathData2.map((eachPoint) => {
  628. return eachPoint.y
  629. }).map((eachY) => {
  630. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  631. })
  632. // 组合成最终数据用来渲染
  633. let pathDataForRender = []
  634. for (let index = 0; index < rawPathData.length; index++) {
  635. pathDataForRender.push([pathXArrayInPx[index], pathYArrayInPx[index]])
  636. }
  637. let pathDataForRender2 = []
  638. for (let index = 0; index < rawPathData2.length; index++) {
  639. pathDataForRender2.push([pathXArrayInPx2[index], pathYArrayInPx2[index]])
  640. }
  641. // 进行渲染
  642. gNode.selectAll('circle.path1').data(pathDataForRender).enter().append('circle')
  643. .classed('path1', true)
  644. .attr('cx', (d) => d[0])
  645. .attr('cy', (d) => d[1])
  646. .attr('r', pointDistance * pxPerUnitLength / 2)
  647. .attr('fill', '#000088')
  648. .attr('pointer-events', 'none')
  649. gNode.selectAll('circle.path2').data(pathDataForRender2).enter().append('circle')
  650. .classed('path2', true)
  651. .attr('cx', (d) => d[0])
  652. .attr('cy', (d) => d[1])
  653. .attr('r', pointDistance * pxPerUnitLength / 3)
  654. .attr('fill', 'blue')
  655. .attr('pointer-events', 'none')
  656. },
  657. onResetPath() {
  658. this.formData.path1 = ''
  659. this.formData.path2 = ''
  660. gNode.selectAll('circle.path1').remove()
  661. gNode.selectAll('circle.path2').remove()
  662. },
  663. renderStartEndPoint() {
  664. gNode.selectAll('circle.start').remove()
  665. gNode.selectAll('circle.end').remove()
  666. startPoint = this.formData.startPoint.trim()
  667. if(startPoint[0] === '[') { startPoint = startPoint.substr(1)}
  668. if (startPoint[startPoint.length - 1] === ']') { startPoint = startPoint.substr(0, startPoint.length - 1) }
  669. startPoint = startPoint.trim()
  670. startPoint = startPoint.split(',').map((item) => {
  671. item = item.trim()
  672. if (!item) {
  673. return undefined
  674. } else {
  675. return Number(item)
  676. }
  677. })
  678. startPoint = startPoint.filter((item) => {
  679. return item !== undefined
  680. })
  681. if (startPoint.length !== 2) {
  682. window.alert(`解析起点坐标失败`)
  683. startPoint = null
  684. }
  685. endPoint = this.formData.endPoint.trim()
  686. if(endPoint[0] === '[') { endPoint = endPoint.substr(1)}
  687. if (endPoint[endPoint.length - 1] === ']') { endPoint = endPoint.substr(0, endPoint.length - 1) }
  688. endPoint = endPoint.trim()
  689. endPoint = endPoint.split(',').map((item) => {
  690. item = item.trim()
  691. if (!item) {
  692. return undefined
  693. } else {
  694. return Number(item)
  695. }
  696. })
  697. endPoint = endPoint.filter((item) => {
  698. return item !== undefined
  699. })
  700. if (endPoint.length !== 2) {
  701. window.alert(`解析终点坐标失败`)
  702. endPoint = null
  703. }
  704. if (startPoint) {
  705. // 起点坐标映射到视口坐标
  706. startPoint[0] = (startPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  707. startPoint[1] = (startPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  708. // 进行渲染
  709. gNode.selectAll('circle.start').data([startPoint]).enter().append('circle')
  710. .classed('start', true)
  711. .attr('cx', (d) => d[0])
  712. .attr('cy', (d) => d[1])
  713. .attr('r', pointDistance * pxPerUnitLength / 3.5)
  714. .attr('fill', 'rgba(50, 255, 50, 1)')
  715. .attr('pointer-events', 'none')
  716. }
  717. if (endPoint) {
  718. // 终点坐标映射到视口坐标
  719. endPoint[0] = (endPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  720. endPoint[1] = (endPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  721. // 进行渲染
  722. gNode.selectAll('circle.end').data([endPoint]).enter().append('circle')
  723. .classed('end', true)
  724. .attr('cx', (d) => d[0])
  725. .attr('cy', (d) => d[1])
  726. .attr('r', pointDistance * pxPerUnitLength / 3.5)
  727. .attr('fill', 'green')
  728. .attr('pointer-events', 'none')
  729. }
  730. },
  731. onResetStartEndPoint() {
  732. this.formData.startPoint = ''
  733. this.formData.endPoint = ''
  734. gNode.selectAll('circle.start').remove()
  735. gNode.selectAll('circle.end').remove()
  736. },
  737. zoomIn(coordinate) {
  738. if (!Array.isArray(coordinate) || coordinate.length !== 2) {
  739. return
  740. }
  741. svgNode.transition().duration(1000).call(
  742. zoomObj.transform,
  743. d3.zoomIdentity.translate(svgWidth / 2, svgHeight / 2).scale(100 / pxPerUnitLength).translate(- coordinate[0], - coordinate[1])
  744. );
  745. },
  746. zoomInToStartPoint() {
  747. this.zoomIn(startPoint)
  748. },
  749. zoomInToEndPoint() {
  750. this.zoomIn(endPoint)
  751. },
  752. resetZoom() {
  753. svgNode.transition().duration(1000).call(
  754. zoomObj.transform,
  755. d3.zoomIdentity.scale(1)
  756. )
  757. },
  758. onSetHeightFilter() {
  759. gNode.selectAll('rect').attr('visibility', (d) => {
  760. if ((this.formData.maxHeight !== '') && (Number(this.formData.maxHeight) < d[2])) {
  761. return 'hidden'
  762. } else if ((this.formData.minHeight !== '') && (Number(this.formData.minHeight) > d[2])) {
  763. return 'hidden'
  764. } else {
  765. return 'visible'
  766. }
  767. })
  768. },
  769. onResetHeightFilter() {
  770. this.formData.maxHeight = ''
  771. this.formData.minHeight = ''
  772. gNode.selectAll('rect').attr('visibility', (d) => {
  773. return 'visible'
  774. })
  775. },
  776. onAddPointUndo() {
  777. if (this.rawWholeDataHistory.currentIdx > 0) {
  778. this.rawWholeDataHistory.currentIdx--
  779. rawWholeData = deepClone(this.rawWholeDataHistory.history[this.rawWholeDataHistory.currentIdx])
  780. gNode.selectAll('rect').remove()
  781. gNode.selectAll('text').remove()
  782. this.renderWholePoints()
  783. }
  784. },
  785. onAddPointRedo() {
  786. if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
  787. this.rawWholeDataHistory.currentIdx++
  788. rawWholeData = deepClone(this.rawWholeDataHistory.history[this.rawWholeDataHistory.currentIdx])
  789. gNode.selectAll('rect').remove()
  790. this.renderWholePoints()
  791. }
  792. },
  793. onAddPoint() {
  794. // 解析svg的transform信息
  795. let translateX = 0
  796. let translateY = 0
  797. let scale = 1
  798. const gNodetransformStr = gNode.attr('transform')
  799. if (gNodetransformStr) {
  800. const translateStr = gNodetransformStr.split(' ')[0]
  801. const translateRegex = /translate\(([^,]+),([^,]+)\)/;
  802. const translateMatch = translateStr.match(translateRegex);
  803. const scaleStr = gNodetransformStr.split(' ')[1]
  804. const scaleRegex = /scale\(([^)]+)\)/;
  805. const scaleMatch = scaleStr.match(scaleRegex);
  806. translateX = Number(translateMatch[1]);
  807. translateY = Number(translateMatch[2]);
  808. scale = Number(scaleMatch[1]);
  809. }
  810. console.log('transform info: ', translateX, translateY, scale);
  811. // 选择框位置复原到svg transfrom前
  812. const brushLeftPxBeformTransform = (brushLeftPx - translateX) / scale
  813. const brushTopPxBeformTransform = (brushTopPx - translateY) / scale
  814. const brushRightPxBeformTransform = (brushRightPx - translateX) / scale
  815. const brushBottomPxBeformTransform = (brushBottomPx - translateY) / scale
  816. // 选择框位置复原到原始坐标系
  817. const brushLeft = (brushLeftPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
  818. const brushTop = (brushTopPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
  819. const brushRight = (brushRightPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
  820. const brushBottom = (brushBottomPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
  821. console.log('brush area in raw coordinate: ', brushLeft, brushTop, brushRight, brushBottom);
  822. // 计算出选择框的外围影响区域
  823. const affectionAreaLeft = brushLeft - pointDistance * Math.SQRT2 * 1.25
  824. const affectionAreaTop = brushTop - pointDistance * Math.SQRT2 * 1.25
  825. const affectionAreaRight = brushRight + pointDistance * Math.SQRT2 * 1.25
  826. const affectionAreaBottom = brushBottom + pointDistance * Math.SQRT2 * 1.25
  827. console.log('affection area in raw coordinate: ', affectionAreaLeft, affectionAreaTop, affectionAreaRight, affectionAreaBottom);
  828. // 筛选出框选区域可能影响到的所有外围点
  829. // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
  830. // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  831. const affectedPointList = []
  832. for (const point of rawWholeData) {
  833. // 如果z不合格,pass
  834. if (Math.abs(point.z - Number(this.formData.addPointHeight)) > Number(this.formData.connectionMaxHeightGap)) {
  835. continue
  836. }
  837. // 如果在affection范围外,pass
  838. if (point.x < affectionAreaLeft || point.x > affectionAreaRight || point.y < affectionAreaTop || point.y > affectionAreaBottom) {
  839. continue
  840. }
  841. // 如果在框选区域内,pass
  842. if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
  843. continue
  844. }
  845. affectedPointList.push(point)
  846. }
  847. // todo: 如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  848. console.log('affectedPointList: ', affectedPointList);
  849. // 筛选出框选区域内所有已存在的点
  850. // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
  851. // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  852. const pointInBrushList = []
  853. for (const point of rawWholeData) {
  854. // 如果z不合格,pass
  855. if (Math.abs(point.z - Number(this.formData.addPointHeight)) > Number(this.formData.connectionMaxHeightGap)) {
  856. continue
  857. }
  858. // 如果在框选区域内,留下
  859. if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
  860. pointInBrushList.push(point)
  861. }
  862. }
  863. if (!pointInBrushList.length) {
  864. window.alert('请保证:1.框选区域内至少已存在一个点位;2.该点位高度与补点高度之间差距不超过高度差上限。该点位将作为点位生长起点。')
  865. return
  866. }
  867. // 拿到框选区域中已存在点的datasetId(所有点都一样),并计算weight平均值
  868. const pointDatasetId = pointInBrushList[0].datasetId
  869. let pointWeightSum = 0
  870. for (const pointInBrush of pointInBrushList) {
  871. pointWeightSum += pointInBrush.weight
  872. }
  873. const pointWeight = pointWeightSum / pointInBrushList.length
  874. // 开始补点
  875. let activePointIdx = 0
  876. let newPointId = rawWholeData[rawWholeData.length - 1].id
  877. // 位于框选区域内的所有点位构成稀疏图模型,用邻接表表示,依次处理其顶点表中各顶点。
  878. while(activePointIdx <= pointInBrushList.length - 1) {
  879. // 拿到当前顶点
  880. const activePoint = pointInBrushList[activePointIdx]
  881. console.log('顶点表:', pointInBrushList);
  882. console.log('当前处理顶点Idx:', activePointIdx);
  883. console.log('当前处理顶点:', activePoint);
  884. // 当前顶点有可能是本次补点前就存在的,所以清空其邻居信息
  885. activePoint.ids = []
  886. const tempIds1 = [] // 临时记录上下左右邻居
  887. const tempIds2 = [] // 临时记录斜角邻居
  888. // 拿到当前顶点的8个相邻点位
  889. const neighbourPosList = getNeighbourLocations(pointInBrushList[activePointIdx], pointDistance, rowSlope)
  890. // 处理8个相邻点位
  891. for (const key in neighbourPosList) {
  892. if (Object.hasOwnProperty.call(neighbourPosList, key)) {
  893. // 看看这个相邻点位属于上下左右邻居还是斜角邻居。
  894. let neighbourType = 0
  895. if (['posRight', 'posBottom', 'posLeft', 'posTop'].includes(key)) {
  896. neighbourType = 1
  897. } else {
  898. neighbourType = 2
  899. }
  900. const neiPos = neighbourPosList[key];
  901. // 如果点位在框选区域外
  902. if (neiPos.x < brushLeft || neiPos.x > brushRight || neiPos.y < brushTop || neiPos.y > brushBottom) {
  903. // 在外围点位表中找匹配的点
  904. const matchedPoint = affectedPointList.find((affectedPoint) => {
  905. return getDistance2D(affectedPoint, neiPos) < pointDistance * 0.1
  906. })
  907. // 如果找到了匹配点
  908. if (matchedPoint) {
  909. console.log(matchedPoint, 'sdfsdf');
  910. // 记录自己与其关系。
  911. if (neighbourType === 1) {
  912. tempIds1.push(String(matchedPoint.id))
  913. } else {
  914. tempIds2.push(String(matchedPoint.id))
  915. }
  916. // 酌情修改这个外围点位与自己的关系。
  917. // 相邻点位数组ids中,前4项表示上下左右邻居,后四项表示斜角邻居。
  918. let idsStartIdx = -1
  919. let idsEndIdx = -1
  920. if (neighbourType === 1) {
  921. idsStartIdx = 0
  922. idsEndIdx = 4
  923. } else {
  924. idsStartIdx = 4
  925. idsEndIdx = 8
  926. }
  927. if (!matchedPoint.ids.slice(idsStartIdx, idsEndIdx).find((id) => {
  928. return id === String(activePoint.id)
  929. })) {
  930. const emptyIdx = matchedPoint.ids.slice(idsStartIdx, idsEndIdx).findIndex((id) => {
  931. return id === '-1'
  932. })
  933. console.assert(emptyIdx !== -1, 'emptyIdx居然不存在?!')
  934. matchedPoint.ids[idsStartIdx + emptyIdx] = String(activePoint.id)
  935. }
  936. }
  937. // 如果没找到匹配点
  938. else {
  939. // 记录自己与其关系。
  940. if (neighbourType === 1) {
  941. tempIds1.push('-1')
  942. } else {
  943. tempIds2.push('-1')
  944. }
  945. }
  946. }
  947. // 如果点位在框选区域内
  948. else {
  949. // 在顶点表中找匹配的点
  950. const matchedPoint = pointInBrushList.find((pointInBrush) => {
  951. return getDistance2D(pointInBrush, neiPos) < pointDistance * 0.3
  952. })
  953. // 如果找到了匹配点
  954. if (matchedPoint) {
  955. // 记录自己与其关系。
  956. if (neighbourType === 1) {
  957. tempIds1.push(String(matchedPoint.id))
  958. } else {
  959. tempIds2.push(String(matchedPoint.id))
  960. }
  961. }
  962. // 如果没找到匹配点
  963. else {
  964. newPointId++
  965. // 创建新点位,加入顶点表
  966. pointInBrushList.push({
  967. id: newPointId,
  968. x: neiPos.x,
  969. y: neiPos.y,
  970. z: Number(this.formData.addPointHeight),
  971. datasetId: pointDatasetId,
  972. weight: pointWeight,
  973. })
  974. // 记录自己与其关系
  975. if (neighbourType === 1) {
  976. tempIds1.push(String(newPointId))
  977. } else {
  978. tempIds2.push(String(newPointId))
  979. }
  980. }
  981. }
  982. }
  983. } // end of 处理8个相邻点位
  984. console.assert(tempIds1.length === 4, 'tempIds1长度咋不是4呢?')
  985. console.assert(tempIds2.length === 4, 'tempIds1长度咋不是4呢?')
  986. tempIds1.sort((a, b) => {
  987. if (a === '-1' && b === '-1') {
  988. return 0
  989. } else if (a === '-1') {
  990. return 1
  991. } else if (b === '-1') {
  992. return -1
  993. } else {
  994. return 0
  995. }
  996. })
  997. tempIds2.sort((a, b) => {
  998. if (a === '-1' && b === '-1') {
  999. return 0
  1000. } else if (a === '-1') {
  1001. return 1
  1002. } else if (b === '-1') {
  1003. return -1
  1004. } else {
  1005. return 0
  1006. }
  1007. })
  1008. activePoint.ids = [...tempIds1, ...tempIds2]
  1009. activePointIdx++
  1010. } // end of 依次处理顶点表中各顶点
  1011. // 新补的点加入rawWholeData
  1012. const oldIdMax = rawWholeData.length // 旧有的点的id最大值,据此判断哪些点是新增的
  1013. for (const pointInBrush of pointInBrushList) {
  1014. if (pointInBrush.id > oldIdMax) {
  1015. pointInBrush.isManuallyAdded = true
  1016. rawWholeData.push(pointInBrush)
  1017. }
  1018. }
  1019. // 渲染
  1020. this.renderWholePoints()
  1021. // 存入历史记录
  1022. if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
  1023. this.rawWholeDataHistory.history = this.rawWholeDataHistory.history.slice(0, this.rawWholeDataHistory.currentIdx + 1)
  1024. }
  1025. this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
  1026. this.rawWholeDataHistory.currentIdx++
  1027. }, // end of method addPoint
  1028. restoreRawWholeData() {
  1029. if (!this.formData.sceneCode.trim()) {
  1030. window.alert('场景码必填!')
  1031. return
  1032. }
  1033. this.loadingHandler = ElLoading.service({
  1034. lock: true,
  1035. text: 'Loading',
  1036. background: 'rgba(0, 0, 0, 0.7)',
  1037. })
  1038. resetWholeData(this.formData.sceneCode).then((res) => {
  1039. ElMessage({
  1040. message: res,
  1041. type: 'success',
  1042. })
  1043. this.getWholeData()
  1044. }).catch((err) => {
  1045. ElMessage({
  1046. message: err,
  1047. type: 'error',
  1048. })
  1049. }).finally(() => {
  1050. this.loadingHandler.close()
  1051. })
  1052. },
  1053. uploadAddPointResult() {
  1054. if (!this.formData.sceneCode.trim()) {
  1055. window.alert('场景码必填!')
  1056. return
  1057. }
  1058. this.loadingHandler = ElLoading.service({
  1059. lock: true,
  1060. text: 'Loading',
  1061. background: 'rgba(0, 0, 0, 0.7)',
  1062. })
  1063. uploadWholeData(this.formData.sceneCode, rawWholeData).then((res) => {
  1064. ElMessage({
  1065. message: res,
  1066. type: 'success',
  1067. })
  1068. }).catch((err) => {
  1069. ElMessage({
  1070. message: err,
  1071. type: 'error',
  1072. })
  1073. }).finally(() => {
  1074. this.loadingHandler.close()
  1075. })
  1076. },
  1077. onPointEditorConfirm(data) {
  1078. rawWholeData[data.id - 1].x = data.x
  1079. rawWholeData[data.id - 1].y = data.y
  1080. rawWholeData[data.id - 1].z = data.z
  1081. rawWholeData[data.id - 1].weight = data.weight
  1082. rawWholeData[data.id - 1].ids = data.ids
  1083. // 渲染
  1084. gNode.selectAll('rect').remove()
  1085. gNode.selectAll('text').remove()
  1086. this.renderWholePoints()
  1087. // 存入历史记录
  1088. if (this.rawWholeDataHistory.currentIdx < this.rawWholeDataHistory.history.length - 1) {
  1089. this.rawWholeDataHistory.history = this.rawWholeDataHistory.history.slice(0, this.rawWholeDataHistory.currentIdx + 1)
  1090. }
  1091. this.rawWholeDataHistory.history.push(deepClone(rawWholeData))
  1092. this.rawWholeDataHistory.currentIdx++
  1093. this.isEditingPoint = false
  1094. }
  1095. },
  1096. }
  1097. </script>
  1098. <style>
  1099. #app {
  1100. font-family: Avenir, Helvetica, Arial, sans-serif;
  1101. -webkit-font-smoothing: antialiased;
  1102. -moz-osx-font-smoothing: grayscale;
  1103. color: #2c3e50;
  1104. }
  1105. .formsWrapper {
  1106. display: flex;
  1107. align-items: center;
  1108. justify-content: space-between;
  1109. }
  1110. .formsWrapper .form1 {
  1111. width: 25vw;
  1112. }
  1113. .formsWrapper .form1 .color-desc{
  1114. margin-top: 10px;
  1115. font-size: 12px;
  1116. }
  1117. .formsWrapper .form2 {
  1118. width: 45vw;
  1119. }
  1120. .formsWrapper .form3 {
  1121. width: 25vw;
  1122. }
  1123. .infoText {
  1124. min-height: 2em;
  1125. }
  1126. .map {
  1127. display: flex;
  1128. align-items: center;
  1129. }
  1130. .map > .map-control-area {
  1131. margin-left: 20px;
  1132. }
  1133. .map > .map-control-area > .panel {
  1134. border: 1px solid black;
  1135. display: flex;
  1136. flex-direction: column;
  1137. justify-content: center;
  1138. align-items: center;
  1139. margin-bottom: 10px;
  1140. }
  1141. .map > .map-control-area > .panel > .btn {
  1142. margin: 5px;
  1143. }
  1144. .map > .map-control-area > .panel {
  1145. padding-left: 10px;
  1146. padding-right: 10px;
  1147. padding-bottom: 10px;
  1148. }
  1149. .map > .map-control-area > .panel input{
  1150. width: 80px;
  1151. }
  1152. .map > .map-control-area > .panel > .btn-group {
  1153. display: flex;
  1154. justify-content: space-between;
  1155. }
  1156. .map > .map-control-area > .add-point.panel > .btn-group {
  1157. margin-bottom: 10px;
  1158. }
  1159. .map > .map-control-area > .edit-button-group > .button-row{
  1160. margin-bottom: 10px;
  1161. display: flex;
  1162. justify-content: space-evenly;
  1163. }
  1164. .map > .map-control-area > .edit-button-group > .button-row > .btn {
  1165. }
  1166. .svgWrapper {
  1167. display: inline-block;
  1168. overflow: hidden;
  1169. background: #eee;
  1170. }
  1171. </style>