App.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. <template>
  2. <div class="formsWrapper">
  3. <el-form
  4. class="form1"
  5. label-position="left"
  6. >
  7. <el-form-item label="场景名或完整url">
  8. <el-input
  9. v-model="sceneNameOrUrl"
  10. autofocus
  11. />
  12. </el-form-item>
  13. <el-button
  14. type="primary"
  15. @click="getWholeData"
  16. >
  17. 重新获取全部数据点
  18. </el-button>
  19. </el-form>
  20. <el-form
  21. class="form2"
  22. label-position="top"
  23. >
  24. <el-form-item
  25. :label="`路径1数据(必填)(节点index,以英文逗号分隔)(浅蓝色大圆圈表示)(路径长度:${path1Length})`"
  26. >
  27. <el-input v-model="formData.path1" />
  28. </el-form-item>
  29. <el-form-item
  30. :label="`路径2数据(选填)(节点index,以英文逗号分隔)(深蓝色小圆圈表示)(路径长度:${path2Length})`"
  31. >
  32. <el-input v-model="formData.path2" />
  33. </el-form-item>
  34. <el-button
  35. type="primary"
  36. @click="renderPath"
  37. >
  38. 显示路径
  39. </el-button>
  40. <el-button @click="onResetPath">
  41. 清空
  42. </el-button>
  43. </el-form>
  44. <el-form
  45. class="form3"
  46. label-position="top"
  47. >
  48. <el-form-item label="起点坐标(以英文逗号分隔)(浅绿色小圆圈表示)">
  49. <el-input v-model="formData.startPoint" />
  50. </el-form-item>
  51. <el-form-item label="终点坐标(以英文逗号分隔)(深绿色小圆圈表示)">
  52. <el-input v-model="formData.endPoint" />
  53. </el-form-item>
  54. <el-button
  55. type="primary"
  56. @click="renderStartEndPoint"
  57. >
  58. 显示起点终点
  59. </el-button>
  60. <el-button @click="onResetStartEndPoint">
  61. 清空
  62. </el-button>
  63. </el-form>
  64. </div>
  65. <div class="infoText">
  66. {{ infoText }}
  67. </div>
  68. <div class="map">
  69. <div class="svgWrapper" />
  70. <div class="map-control-area">
  71. <div class="zoom-control panel">
  72. 聚焦操作
  73. <el-button
  74. class="btn"
  75. type="primary"
  76. @click="zoomInToStartPoint"
  77. >
  78. 聚焦到起点
  79. </el-button>
  80. <el-button
  81. class="btn"
  82. type="primary"
  83. @click="zoomInToEndPoint"
  84. >
  85. 聚焦到终点
  86. </el-button>
  87. <el-button
  88. class="btn"
  89. @click="resetZoom"
  90. >
  91. 取消聚焦
  92. </el-button>
  93. </div>
  94. <el-form
  95. class="height-filter panel"
  96. label-position="top"
  97. >
  98. 高度筛选
  99. <el-form-item
  100. :label="`高度上限`"
  101. >
  102. <el-input
  103. v-model="formData.maxHeight"
  104. type="number"
  105. />
  106. </el-form-item>
  107. <el-form-item
  108. :label="`高度下限`"
  109. >
  110. <el-input
  111. v-model="formData.minHeight"
  112. type="number"
  113. />
  114. </el-form-item>
  115. <div class="btn-group">
  116. <el-button
  117. type="primary"
  118. @click="onSetHeightFilter"
  119. >
  120. 确定
  121. </el-button>
  122. <el-button @click="onResetHeightFilter">
  123. 清空
  124. </el-button>
  125. </div>
  126. </el-form>
  127. </div>
  128. </div>
  129. </template>
  130. <script>
  131. import * as d3 from "d3";
  132. import { getWholeData } from "./api.js";
  133. import { ElLoading } from 'element-plus'
  134. const LENGTH_PER_POINT = 0.36
  135. // 视口尺寸
  136. let svgWidth = document.documentElement.clientWidth - 200
  137. let svgHeight = document.documentElement.clientHeight - 280
  138. let svgRatio = svgWidth / svgHeight
  139. let pxPerUnitLength = 0 // 原始单位长度对应的像素数
  140. let rawWholeData = []
  141. let wholeDataForRender = []
  142. let startPoint = null
  143. let endPoint = null
  144. // 全部数据点的分布中心
  145. let xCenter = 0
  146. let yCenter = 0
  147. function resetGlobalVars() {
  148. pxPerUnitLength = 0 // 原始单位长度对应的像素数
  149. rawWholeData = []
  150. wholeDataForRender = []
  151. xCenter = 0
  152. yCenter = 0
  153. startPoint = null
  154. endPoint = null
  155. }
  156. let svgNode = null
  157. let gNode = null
  158. let zoomObj = null
  159. export default {
  160. name: 'App',
  161. data() {
  162. return {
  163. sceneNameOrUrl: 'SS-t-NZUICC2fRLi',
  164. infoText: '',
  165. loadingHandler: null,
  166. formData: {
  167. path1: '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',
  168. path2: '',
  169. startPoint: '-38.5, 10.8',
  170. endPoint: '-28.4, 12.2',
  171. maxHeight: '',
  172. minHeight: '',
  173. }
  174. }
  175. },
  176. computed: {
  177. path1Length() {
  178. return this.getInputPathLength(this.formData.path1)
  179. },
  180. path2Length() {
  181. return this.getInputPathLength(this.formData.path2)
  182. },
  183. },
  184. mounted() {
  185. svgNode = d3.select('.svgWrapper').append("svg")
  186. .attr("width", svgWidth)
  187. .attr('height', svgHeight)
  188. gNode = svgNode.append('g')
  189. // this.getWholeData()
  190. },
  191. methods: {
  192. getInputPathLength(input) {
  193. let temp = input.trim()
  194. if(temp[0] === '[') { temp = temp.substr(1)}
  195. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  196. temp = temp.trim()
  197. if (!temp) {
  198. return 0
  199. }
  200. let mapSuccess = true
  201. temp = temp.split(',').map((item) => {
  202. item = item.trim()
  203. if (!item) {
  204. return undefined
  205. } else {
  206. const num = Number(item)
  207. if (!Number.isSafeInteger(num)) {
  208. mapSuccess = false
  209. return undefined
  210. } else {
  211. return num
  212. }
  213. }
  214. })
  215. if (!mapSuccess) {
  216. return '?'
  217. } else {
  218. return temp.filter((item) => {
  219. return item !== undefined
  220. }).length
  221. }
  222. },
  223. inputPathStringToArray(input) {
  224. let temp = input.trim()
  225. if(temp[0] === '[') { temp = temp.substr(1)}
  226. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  227. temp = temp.trim()
  228. if (!temp) {
  229. return []
  230. }
  231. let mapSuccess = true
  232. temp = temp.split(',').map((item) => {
  233. item = item.trim()
  234. if (!item) {
  235. return undefined
  236. } else {
  237. const num = Number(item)
  238. if (!Number.isSafeInteger(num)) {
  239. mapSuccess = false
  240. return undefined
  241. } else {
  242. return num
  243. }
  244. }
  245. })
  246. if (!mapSuccess) {
  247. window.alert(`解析输入路径失败:${input}`)
  248. return -1
  249. } else {
  250. return temp.filter((item) => {
  251. return item !== undefined
  252. })
  253. }
  254. },
  255. getWholeData() {
  256. if (!this.sceneNameOrUrl.trim()) {
  257. window.alert('场景名或完整url必填!')
  258. return
  259. }
  260. this.loadingHandler = ElLoading.service({
  261. lock: true,
  262. text: 'Loading',
  263. background: 'rgba(0, 0, 0, 0.7)',
  264. })
  265. resetGlobalVars()
  266. gNode.selectAll('rect').remove()
  267. gNode.selectAll('circle').remove()
  268. const that = this
  269. getWholeData(this.sceneNameOrUrl).then((res) => {
  270. rawWholeData = res
  271. // 所有点的分布情况
  272. let xArray = rawWholeData.map((eachPoint) => {
  273. return eachPoint.x
  274. })
  275. let xLength = Math.max(...xArray) - Math.min(...xArray)
  276. xCenter = (Math.max(...xArray) + Math.min(...xArray)) / 2
  277. let yArray = rawWholeData.map((eachPoint) => {
  278. return eachPoint.y
  279. })
  280. let yLength = Math.max(...yArray) - Math.min(...yArray)
  281. yCenter = (Math.max(...yArray) + Math.min(...yArray)) / 2
  282. let zArray = rawWholeData.map((eachPoint) => {
  283. return eachPoint.z
  284. })
  285. let zLength = Math.max(...zArray) - Math.min(...zArray)
  286. let zMin = Math.min(...zArray)
  287. let areaRatio = xLength / yLength
  288. // 各个点坐标映射到视口坐标
  289. if (svgRatio >= areaRatio) { // 分布范围应略小于svg尺寸
  290. pxPerUnitLength = svgHeight / yLength * 0.9
  291. } else {
  292. pxPerUnitLength = svgWidth / xLength * 0.9
  293. }
  294. let wholeXArrayInPx = xArray.map((eachX) => {
  295. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  296. })
  297. let wholeYArrayInPx = yArray.map((eachY) => {
  298. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  299. })
  300. // 组合成最终数据用来渲染
  301. for (let index = 0; index < rawWholeData.length; index++) {
  302. console.assert(rawWholeData[index].id === (index + 1), '数据点id和数据点在数组中的位置不相符!')
  303. wholeDataForRender.push([wholeXArrayInPx[index], wholeYArrayInPx[index], zArray[index], JSON.stringify(rawWholeData[index]), rawWholeData[index].id])
  304. }
  305. gNode.selectAll('rect').data(wholeDataForRender).enter().append('rect')
  306. .attr('x', (d) => d[0] - LENGTH_PER_POINT * pxPerUnitLength / 2)
  307. .attr('y', (d) => d[1] - LENGTH_PER_POINT * pxPerUnitLength / 2)
  308. .attr('width', LENGTH_PER_POINT * pxPerUnitLength)
  309. .attr('height', LENGTH_PER_POINT * pxPerUnitLength)
  310. .attr('fill', (d) => {
  311. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
  312. })
  313. .attr('render-data', (d) => {
  314. return d
  315. })
  316. gNode.selectAll('rect').on('mouseover', function(e) {
  317. d3.select(this).attr('fill', 'orange')
  318. let renderDataItem = e.target.attributes['render-data'].value
  319. let renderDataItemArray = renderDataItem.split(',')
  320. that.infoText = `数据点id: ${renderDataItemArray[renderDataItemArray.length - 1]}, \n具体值: ${renderDataItem.match(/^[^{]+(\{.+\})[^}]+$/)[1]}`
  321. }).on('mouseleave', function (e) {
  322. d3.select(this).attr('fill', (d) => {
  323. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
  324. })
  325. that.infoText = ''
  326. })
  327. zoomObj = d3.zoom().on("zoom", zoomed)
  328. svgNode.call(zoomObj);
  329. function zoomed({transform}) {
  330. gNode.attr("transform", transform);
  331. }
  332. }).finally(() => {
  333. this.loadingHandler.close()
  334. })
  335. },
  336. renderPath() {
  337. if (!this.formData.path1.trim()) {
  338. window.alert('路径1必填!')
  339. return
  340. }
  341. gNode.selectAll('circle.path1').remove()
  342. gNode.selectAll('circle.path2').remove()
  343. let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
  344. if (rawPathDataIndex === -1) {
  345. return
  346. }
  347. let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
  348. if (rawPathDataIndex2 === -1) {
  349. return
  350. }
  351. // 基于path index 拿到path节点数组
  352. let rawPathData = []
  353. for (let index = 0; index < rawPathDataIndex.length; index++) {
  354. const element = rawWholeData[rawPathDataIndex[index] - 1];
  355. // 假设节点id和节点在数组中出现顺序相符
  356. console.assert(element.id === rawPathDataIndex[index], '按照id寻找路径节点失败!')
  357. rawPathData.push(element)
  358. }
  359. let rawPathData2 = []
  360. for (let index = 0; index < rawPathDataIndex2.length; index++) {
  361. const element = rawWholeData[rawPathDataIndex2[index] - 1];
  362. // 假设节点id和节点在数组中出现顺序相符
  363. console.assert(element.id === rawPathDataIndex2[index], '按照id寻找路径节点失败!')
  364. rawPathData2.push(element)
  365. }
  366. // 各个点坐标映射到视口坐标
  367. let pathXArrayInPx = rawPathData.map((eachPoint) => {
  368. return eachPoint.x
  369. }).map((eachX) => {
  370. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  371. })
  372. let pathYArrayInPx = rawPathData.map((eachPoint) => {
  373. return eachPoint.y
  374. }).map((eachY) => {
  375. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  376. })
  377. let pathXArrayInPx2 = rawPathData2.map((eachPoint) => {
  378. return eachPoint.x
  379. }).map((eachX) => {
  380. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  381. })
  382. let pathYArrayInPx2 = rawPathData2.map((eachPoint) => {
  383. return eachPoint.y
  384. }).map((eachY) => {
  385. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  386. })
  387. // 组合成最终数据用来渲染
  388. let pathDataForRender = []
  389. for (let index = 0; index < rawPathData.length; index++) {
  390. pathDataForRender.push([pathXArrayInPx[index], pathYArrayInPx[index]])
  391. }
  392. let pathDataForRender2 = []
  393. for (let index = 0; index < rawPathData2.length; index++) {
  394. pathDataForRender2.push([pathXArrayInPx2[index], pathYArrayInPx2[index]])
  395. }
  396. // 进行渲染
  397. gNode.selectAll('circle.path1').data(pathDataForRender).enter().append('circle')
  398. .classed('path1', true)
  399. .attr('cx', (d) => d[0])
  400. .attr('cy', (d) => d[1])
  401. .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 2)
  402. .attr('fill', '#000088')
  403. .attr('pointer-events', 'none')
  404. gNode.selectAll('circle.path2').data(pathDataForRender2).enter().append('circle')
  405. .classed('path2', true)
  406. .attr('cx', (d) => d[0])
  407. .attr('cy', (d) => d[1])
  408. .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3)
  409. .attr('fill', 'blue')
  410. .attr('pointer-events', 'none')
  411. },
  412. onResetPath() {
  413. this.formData.path1 = ''
  414. this.formData.path2 = ''
  415. gNode.selectAll('circle.path1').remove()
  416. gNode.selectAll('circle.path2').remove()
  417. },
  418. renderStartEndPoint() {
  419. gNode.selectAll('circle.start').remove()
  420. gNode.selectAll('circle.end').remove()
  421. startPoint = this.formData.startPoint.trim()
  422. if(startPoint[0] === '[') { startPoint = startPoint.substr(1)}
  423. if (startPoint[startPoint.length - 1] === ']') { startPoint = startPoint.substr(0, startPoint.length - 1) }
  424. startPoint = startPoint.trim()
  425. startPoint = startPoint.split(',').map((item) => {
  426. item = item.trim()
  427. if (!item) {
  428. return undefined
  429. } else {
  430. return Number(item)
  431. }
  432. })
  433. startPoint = startPoint.filter((item) => {
  434. return item !== undefined
  435. })
  436. if (startPoint.length !== 2) {
  437. window.alert(`解析起点坐标失败`)
  438. startPoint = null
  439. }
  440. endPoint = this.formData.endPoint.trim()
  441. if(endPoint[0] === '[') { endPoint = endPoint.substr(1)}
  442. if (endPoint[endPoint.length - 1] === ']') { endPoint = endPoint.substr(0, endPoint.length - 1) }
  443. endPoint = endPoint.trim()
  444. endPoint = endPoint.split(',').map((item) => {
  445. item = item.trim()
  446. if (!item) {
  447. return undefined
  448. } else {
  449. return Number(item)
  450. }
  451. })
  452. endPoint = endPoint.filter((item) => {
  453. return item !== undefined
  454. })
  455. if (endPoint.length !== 2) {
  456. window.alert(`解析终点坐标失败`)
  457. endPoint = null
  458. }
  459. if (startPoint) {
  460. // 起点坐标映射到视口坐标
  461. startPoint[0] = (startPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  462. startPoint[1] = (startPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  463. // 进行渲染
  464. gNode.selectAll('circle.start').data([startPoint]).enter().append('circle')
  465. .classed('start', true)
  466. .attr('cx', (d) => d[0])
  467. .attr('cy', (d) => d[1])
  468. .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3.5)
  469. .attr('fill', 'rgba(50, 255, 50, 1)')
  470. .attr('pointer-events', 'none')
  471. }
  472. if (endPoint) {
  473. // 终点坐标映射到视口坐标
  474. endPoint[0] = (endPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  475. endPoint[1] = (endPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  476. // 进行渲染
  477. gNode.selectAll('circle.end').data([endPoint]).enter().append('circle')
  478. .classed('end', true)
  479. .attr('cx', (d) => d[0])
  480. .attr('cy', (d) => d[1])
  481. .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3.5)
  482. .attr('fill', 'green')
  483. .attr('pointer-events', 'none')
  484. }
  485. },
  486. onResetStartEndPoint() {
  487. this.formData.startPoint = ''
  488. this.formData.endPoint = ''
  489. gNode.selectAll('circle.start').remove()
  490. gNode.selectAll('circle.end').remove()
  491. },
  492. zoomIn(coordinate) {
  493. if (!Array.isArray(coordinate) || coordinate.length !== 2) {
  494. return
  495. }
  496. svgNode.transition().duration(1000).call(
  497. zoomObj.transform,
  498. d3.zoomIdentity.translate(svgWidth / 2, svgHeight / 2).scale(100 / pxPerUnitLength).translate(- coordinate[0], - coordinate[1])
  499. );
  500. },
  501. zoomInToStartPoint() {
  502. this.zoomIn(startPoint)
  503. },
  504. zoomInToEndPoint() {
  505. this.zoomIn(endPoint)
  506. },
  507. resetZoom() {
  508. svgNode.transition().duration(1000).call(
  509. zoomObj.transform,
  510. d3.zoomIdentity.scale(1)
  511. )
  512. },
  513. onSetHeightFilter() {
  514. gNode.selectAll('rect').attr('visibility', (d) => {
  515. if ((this.formData.maxHeight !== '') && (this.formData.maxHeight < d[2])) {
  516. return 'hidden'
  517. } else if ((this.formData.minHeight !== '') && (this.formData.minHeight > d[2])) {
  518. return 'hidden'
  519. } else {
  520. return 'visible'
  521. }
  522. })
  523. },
  524. onResetHeightFilter() {
  525. this.formData.maxHeight = ''
  526. this.formData.minHeight = ''
  527. gNode.selectAll('rect').attr('visibility', (d) => {
  528. return 'visible'
  529. })
  530. }
  531. },
  532. }
  533. </script>
  534. <style>
  535. #app {
  536. font-family: Avenir, Helvetica, Arial, sans-serif;
  537. -webkit-font-smoothing: antialiased;
  538. -moz-osx-font-smoothing: grayscale;
  539. color: #2c3e50;
  540. }
  541. .formsWrapper {
  542. display: flex;
  543. align-items: center;
  544. justify-content: space-between;
  545. }
  546. .formsWrapper .form1 {
  547. width: 25vw;
  548. }
  549. .formsWrapper .form2 {
  550. width: 45vw;
  551. }
  552. .formsWrapper .form3 {
  553. width: 25vw;
  554. }
  555. .infoText {
  556. min-height: 2em;
  557. }
  558. .map {
  559. display: flex;
  560. align-items: center;
  561. }
  562. .map > .map-control-area {
  563. margin-left: 20px;
  564. }
  565. .map > .map-control-area > .panel {
  566. border: 1px solid black;
  567. display: flex;
  568. flex-direction: column;
  569. justify-content: center;
  570. align-items: center;
  571. margin-bottom: 15px;
  572. }
  573. .map > .map-control-area > .panel > .btn {
  574. margin: 10px;
  575. }
  576. .map > .map-control-area > .panel.height-filter {
  577. padding-left: 10px;
  578. padding-right: 10px;
  579. }
  580. .map > .map-control-area > .panel.height-filter > .btn-group {
  581. display: flex;
  582. justify-content: space-between;
  583. margin-bottom: 10px;
  584. }
  585. .svgWrapper {
  586. display: inline-block;
  587. overflow: hidden;
  588. background: #eee;
  589. }
  590. </style>