App.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953
  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. >
  97. 高度筛选
  98. <el-form-item
  99. :label="`高度上限`"
  100. >
  101. <el-input
  102. v-model="formData.maxHeight"
  103. type="number"
  104. />
  105. </el-form-item>
  106. <el-form-item
  107. :label="`高度下限`"
  108. >
  109. <el-input
  110. v-model="formData.minHeight"
  111. type="number"
  112. />
  113. </el-form-item>
  114. <div class="btn-group">
  115. <el-button
  116. type="primary"
  117. @click="onSetHeightFilter"
  118. >
  119. 确定
  120. </el-button>
  121. <el-button @click="onResetHeightFilter">
  122. 清空
  123. </el-button>
  124. </div>
  125. </el-form>
  126. <el-form
  127. class="add-point panel"
  128. >
  129. 手动补点
  130. <el-form-item
  131. :label="`补点模式`"
  132. >
  133. <el-switch
  134. v-model="formData.isAddingPoint"
  135. />
  136. </el-form-item>
  137. <el-form-item
  138. :label="`高度差上限`"
  139. title="要想判定两个相邻区域连通,z坐标值之差不得超过此上限"
  140. >
  141. <el-input
  142. v-model="formData.connectionMaxHeightGap"
  143. type="number"
  144. />
  145. </el-form-item>
  146. <el-form-item
  147. :label="`补点高度`"
  148. >
  149. <el-input
  150. v-model="formData.addPointHeight"
  151. type="number"
  152. />
  153. </el-form-item>
  154. <div class="btn-group">
  155. <el-button
  156. @click="onAddPointUndo"
  157. >
  158. undo
  159. </el-button>
  160. <el-button
  161. @click="onAddPointRedo"
  162. >
  163. redo
  164. </el-button>
  165. <el-button
  166. type="primary"
  167. @click="onAddPoint"
  168. >
  169. 确定
  170. </el-button>
  171. </div>
  172. </el-form>
  173. </div>
  174. </div>
  175. </template>
  176. <script>
  177. import * as d3 from "d3";
  178. import { getWholeData } from "./api.js";
  179. import { ElLoading } from 'element-plus'
  180. import {getDistance2D, computePointDistanceAndRowSlope, getNeighbourLocations} from '@/utils.js'
  181. // 视口尺寸
  182. let svgWidth = document.documentElement.clientWidth - 200
  183. let svgHeight = document.documentElement.clientHeight - 280
  184. let svgRatio = svgWidth / svgHeight
  185. // 全体点位数据
  186. let rawWholeData = []
  187. let wholeDataForRender = []
  188. // 用户输入的起点终点
  189. let startPoint = null
  190. let endPoint = null
  191. // 由原始数据算出的几何信息
  192. let pxPerUnitLength = 0 // 原始数据1单位长度对应的像素数
  193. let pointDistance = 0 // 最近相邻点间距离(单位:原始数据中长度单位)
  194. let rowSlope = 0 // 点位构成的排的斜率 [0deg, 90deg)
  195. let xCenter = 0
  196. let yCenter = 0
  197. // svg、d3相关
  198. let svgNode = null
  199. let gNode = null
  200. let zoomObj = null
  201. let brushObj = null
  202. // d3选择框位置
  203. let brushLeftPx = 0
  204. let brushTopPx = 0
  205. let brushRightPx = 0
  206. let brushBottomPx = 0
  207. function resetGlobalVars() {
  208. pxPerUnitLength = 0 // 原始单位长度对应的像素数
  209. rawWholeData = []
  210. wholeDataForRender = []
  211. xCenter = 0
  212. yCenter = 0
  213. startPoint = null
  214. endPoint = null
  215. pointDistance = 0
  216. rowSlope = 0
  217. }
  218. function zoomed({transform}) {
  219. gNode.attr("transform", transform);
  220. }
  221. function brushed(e) {
  222. if (e.selection) {
  223. console.log('bursh area in px: ', e.selection[0][0], e.selection[0][1], e.selection[1][0], e.selection[1][1]);
  224. brushLeftPx = e.selection[0][0]
  225. brushTopPx = e.selection[0][1]
  226. brushRightPx = e.selection[1][0]
  227. brushBottomPx = e.selection[1][1]
  228. } else {
  229. brushLeftPx = 0
  230. brushTopPx = 0
  231. brushRightPx = 0
  232. brushBottomPx = 0
  233. }
  234. }
  235. export default {
  236. name: 'App',
  237. data() {
  238. return {
  239. sceneNameOrUrl: 'SS-t-XkquhxxurM',
  240. // sceneNameOrUrl: 'SS-t-NZUICC2fRLi',
  241. infoText: '',
  242. loadingHandler: null,
  243. formData: {
  244. 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',
  245. path2: '',
  246. startPoint: '-38.5, 10.8',
  247. endPoint: '-28.4, 12.2',
  248. maxHeight: '',
  249. minHeight: '',
  250. isAddingPoint: false,
  251. connectionMaxHeightGap: 100,
  252. addPointHeight: '',
  253. }
  254. }
  255. },
  256. computed: {
  257. path1Length() {
  258. return this.getInputPathLength(this.formData.path1)
  259. },
  260. path2Length() {
  261. return this.getInputPathLength(this.formData.path2)
  262. },
  263. },
  264. watch: {
  265. 'formData.isAddingPoint': {
  266. handler(vNew) {
  267. if (vNew) {
  268. svgNode.on(".zoom", null)
  269. svgNode.append('g').attr("class", "brush").call(brushObj)
  270. } else {
  271. svgNode.call(zoomObj)
  272. svgNode.selectAll('g.brush').remove()
  273. }
  274. }
  275. }
  276. },
  277. mounted() {
  278. svgNode = d3.select('.svgWrapper').append("svg")
  279. .attr("width", svgWidth)
  280. .attr('height', svgHeight)
  281. gNode = svgNode.append('g')
  282. // this.getWholeData()
  283. },
  284. methods: {
  285. getInputPathLength(input) {
  286. let temp = input.trim()
  287. if(temp[0] === '[') { temp = temp.substr(1)}
  288. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  289. temp = temp.trim()
  290. if (!temp) {
  291. return 0
  292. }
  293. let mapSuccess = true
  294. temp = temp.split(',').map((item) => {
  295. item = item.trim()
  296. if (!item) {
  297. return undefined
  298. } else {
  299. const num = Number(item)
  300. if (!Number.isSafeInteger(num)) {
  301. mapSuccess = false
  302. return undefined
  303. } else {
  304. return num
  305. }
  306. }
  307. })
  308. if (!mapSuccess) {
  309. return '?'
  310. } else {
  311. return temp.filter((item) => {
  312. return item !== undefined
  313. }).length
  314. }
  315. },
  316. inputPathStringToArray(input) {
  317. let temp = input.trim()
  318. if(temp[0] === '[') { temp = temp.substr(1)}
  319. if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
  320. temp = temp.trim()
  321. if (!temp) {
  322. return []
  323. }
  324. let mapSuccess = true
  325. temp = temp.split(',').map((item) => {
  326. item = item.trim()
  327. if (!item) {
  328. return undefined
  329. } else {
  330. const num = Number(item)
  331. if (!Number.isSafeInteger(num)) {
  332. mapSuccess = false
  333. return undefined
  334. } else {
  335. return num
  336. }
  337. }
  338. })
  339. if (!mapSuccess) {
  340. window.alert(`解析输入路径失败:${input}`)
  341. return -1
  342. } else {
  343. return temp.filter((item) => {
  344. return item !== undefined
  345. })
  346. }
  347. },
  348. getWholeData() {
  349. if (!this.sceneNameOrUrl.trim()) {
  350. window.alert('场景名或完整url必填!')
  351. return
  352. }
  353. this.loadingHandler = ElLoading.service({
  354. lock: true,
  355. text: 'Loading',
  356. background: 'rgba(0, 0, 0, 0.7)',
  357. })
  358. resetGlobalVars()
  359. gNode.selectAll('rect').remove()
  360. gNode.selectAll('circle').remove()
  361. const that = this
  362. getWholeData(this.sceneNameOrUrl).then((res) => {
  363. rawWholeData = res
  364. // 相邻点位间距离
  365. const temp = computePointDistanceAndRowSlope(rawWholeData)
  366. pointDistance = temp[0]
  367. rowSlope = temp[1]
  368. // 所有点的分布情况
  369. let xArray = rawWholeData.map((eachPoint) => {
  370. return eachPoint.x
  371. })
  372. let xLength = Math.max(...xArray) - Math.min(...xArray)
  373. xCenter = (Math.max(...xArray) + Math.min(...xArray)) / 2
  374. let yArray = rawWholeData.map((eachPoint) => {
  375. return eachPoint.y
  376. })
  377. let yLength = Math.max(...yArray) - Math.min(...yArray)
  378. yCenter = (Math.max(...yArray) + Math.min(...yArray)) / 2
  379. let zArray = rawWholeData.map((eachPoint) => {
  380. return eachPoint.z
  381. })
  382. let zLength = Math.max(...zArray) - Math.min(...zArray)
  383. let zMin = Math.min(...zArray)
  384. this.formData.addPointHeight = zMin + zLength / 2
  385. let areaRatio = xLength / yLength
  386. // 各个点坐标映射到视口坐标
  387. if (svgRatio >= areaRatio) { // 分布范围应略小于svg尺寸
  388. pxPerUnitLength = svgHeight / yLength * 0.9
  389. } else {
  390. pxPerUnitLength = svgWidth / xLength * 0.9
  391. }
  392. let wholeXArrayInPx = xArray.map((eachX) => {
  393. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  394. })
  395. let wholeYArrayInPx = yArray.map((eachY) => {
  396. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  397. })
  398. // 组合成最终数据用来渲染
  399. for (let index = 0; index < rawWholeData.length; index++) {
  400. console.assert(rawWholeData[index].id === (index + 1), '数据点id和数据点在数组中的位置不相符!')
  401. wholeDataForRender.push([wholeXArrayInPx[index], wholeYArrayInPx[index], zArray[index], JSON.stringify(rawWholeData[index]), rawWholeData[index].id])
  402. }
  403. gNode.selectAll('rect').data(wholeDataForRender).enter().append('rect')
  404. .attr('x', (d) => d[0] - pointDistance * pxPerUnitLength / 2)
  405. .attr('y', (d) => d[1] - pointDistance * pxPerUnitLength / 2)
  406. .attr('width', pointDistance * pxPerUnitLength)
  407. .attr('height', pointDistance * pxPerUnitLength)
  408. .attr('fill', (d) => {
  409. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
  410. })
  411. .attr('render-data', (d) => {
  412. return d
  413. })
  414. gNode.selectAll('rect').on('mouseover', function(e) {
  415. d3.select(this).attr('fill', 'orange')
  416. let renderDataItem = e.target.attributes['render-data'].value
  417. let renderDataItemArray = renderDataItem.split(',')
  418. that.infoText = `数据点id: ${renderDataItemArray[renderDataItemArray.length - 1]}, \n具体值: ${renderDataItem.match(/^[^{]+(\{.+\})[^}]+$/)[1]}`
  419. }).on('mouseleave', function (e) {
  420. d3.select(this).attr('fill', (d) => {
  421. return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
  422. })
  423. that.infoText = ''
  424. })
  425. zoomObj = d3.zoom().on("zoom", zoomed)
  426. svgNode.call(zoomObj);
  427. brushObj = d3.brush().on("end", (e) => {
  428. brushed(e)
  429. })
  430. }).finally(() => {
  431. this.loadingHandler.close()
  432. })
  433. },
  434. renderPath() {
  435. if (!this.formData.path1.trim()) {
  436. window.alert('路径1必填!')
  437. return
  438. }
  439. gNode.selectAll('circle.path1').remove()
  440. gNode.selectAll('circle.path2').remove()
  441. let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
  442. if (rawPathDataIndex === -1) {
  443. return
  444. }
  445. let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
  446. if (rawPathDataIndex2 === -1) {
  447. return
  448. }
  449. // 基于path index 拿到path节点数组
  450. let rawPathData = []
  451. for (let index = 0; index < rawPathDataIndex.length; index++) {
  452. const element = rawWholeData[rawPathDataIndex[index] - 1];
  453. // 假设节点id和节点在数组中出现顺序相符
  454. console.assert(element.id === rawPathDataIndex[index], '按照id寻找路径节点失败!')
  455. rawPathData.push(element)
  456. }
  457. let rawPathData2 = []
  458. for (let index = 0; index < rawPathDataIndex2.length; index++) {
  459. const element = rawWholeData[rawPathDataIndex2[index] - 1];
  460. // 假设节点id和节点在数组中出现顺序相符
  461. console.assert(element.id === rawPathDataIndex2[index], '按照id寻找路径节点失败!')
  462. rawPathData2.push(element)
  463. }
  464. // 各个点坐标映射到视口坐标
  465. let pathXArrayInPx = rawPathData.map((eachPoint) => {
  466. return eachPoint.x
  467. }).map((eachX) => {
  468. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  469. })
  470. let pathYArrayInPx = rawPathData.map((eachPoint) => {
  471. return eachPoint.y
  472. }).map((eachY) => {
  473. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  474. })
  475. let pathXArrayInPx2 = rawPathData2.map((eachPoint) => {
  476. return eachPoint.x
  477. }).map((eachX) => {
  478. return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
  479. })
  480. let pathYArrayInPx2 = rawPathData2.map((eachPoint) => {
  481. return eachPoint.y
  482. }).map((eachY) => {
  483. return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
  484. })
  485. // 组合成最终数据用来渲染
  486. let pathDataForRender = []
  487. for (let index = 0; index < rawPathData.length; index++) {
  488. pathDataForRender.push([pathXArrayInPx[index], pathYArrayInPx[index]])
  489. }
  490. let pathDataForRender2 = []
  491. for (let index = 0; index < rawPathData2.length; index++) {
  492. pathDataForRender2.push([pathXArrayInPx2[index], pathYArrayInPx2[index]])
  493. }
  494. // 进行渲染
  495. gNode.selectAll('circle.path1').data(pathDataForRender).enter().append('circle')
  496. .classed('path1', true)
  497. .attr('cx', (d) => d[0])
  498. .attr('cy', (d) => d[1])
  499. .attr('r', pointDistance * pxPerUnitLength / 2)
  500. .attr('fill', '#000088')
  501. .attr('pointer-events', 'none')
  502. gNode.selectAll('circle.path2').data(pathDataForRender2).enter().append('circle')
  503. .classed('path2', true)
  504. .attr('cx', (d) => d[0])
  505. .attr('cy', (d) => d[1])
  506. .attr('r', pointDistance * pxPerUnitLength / 3)
  507. .attr('fill', 'blue')
  508. .attr('pointer-events', 'none')
  509. },
  510. onResetPath() {
  511. this.formData.path1 = ''
  512. this.formData.path2 = ''
  513. gNode.selectAll('circle.path1').remove()
  514. gNode.selectAll('circle.path2').remove()
  515. },
  516. renderStartEndPoint() {
  517. gNode.selectAll('circle.start').remove()
  518. gNode.selectAll('circle.end').remove()
  519. startPoint = this.formData.startPoint.trim()
  520. if(startPoint[0] === '[') { startPoint = startPoint.substr(1)}
  521. if (startPoint[startPoint.length - 1] === ']') { startPoint = startPoint.substr(0, startPoint.length - 1) }
  522. startPoint = startPoint.trim()
  523. startPoint = startPoint.split(',').map((item) => {
  524. item = item.trim()
  525. if (!item) {
  526. return undefined
  527. } else {
  528. return Number(item)
  529. }
  530. })
  531. startPoint = startPoint.filter((item) => {
  532. return item !== undefined
  533. })
  534. if (startPoint.length !== 2) {
  535. window.alert(`解析起点坐标失败`)
  536. startPoint = null
  537. }
  538. endPoint = this.formData.endPoint.trim()
  539. if(endPoint[0] === '[') { endPoint = endPoint.substr(1)}
  540. if (endPoint[endPoint.length - 1] === ']') { endPoint = endPoint.substr(0, endPoint.length - 1) }
  541. endPoint = endPoint.trim()
  542. endPoint = endPoint.split(',').map((item) => {
  543. item = item.trim()
  544. if (!item) {
  545. return undefined
  546. } else {
  547. return Number(item)
  548. }
  549. })
  550. endPoint = endPoint.filter((item) => {
  551. return item !== undefined
  552. })
  553. if (endPoint.length !== 2) {
  554. window.alert(`解析终点坐标失败`)
  555. endPoint = null
  556. }
  557. if (startPoint) {
  558. // 起点坐标映射到视口坐标
  559. startPoint[0] = (startPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  560. startPoint[1] = (startPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  561. // 进行渲染
  562. gNode.selectAll('circle.start').data([startPoint]).enter().append('circle')
  563. .classed('start', true)
  564. .attr('cx', (d) => d[0])
  565. .attr('cy', (d) => d[1])
  566. .attr('r', pointDistance * pxPerUnitLength / 3.5)
  567. .attr('fill', 'rgba(50, 255, 50, 1)')
  568. .attr('pointer-events', 'none')
  569. }
  570. if (endPoint) {
  571. // 终点坐标映射到视口坐标
  572. endPoint[0] = (endPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
  573. endPoint[1] = (endPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
  574. // 进行渲染
  575. gNode.selectAll('circle.end').data([endPoint]).enter().append('circle')
  576. .classed('end', true)
  577. .attr('cx', (d) => d[0])
  578. .attr('cy', (d) => d[1])
  579. .attr('r', pointDistance * pxPerUnitLength / 3.5)
  580. .attr('fill', 'green')
  581. .attr('pointer-events', 'none')
  582. }
  583. },
  584. onResetStartEndPoint() {
  585. this.formData.startPoint = ''
  586. this.formData.endPoint = ''
  587. gNode.selectAll('circle.start').remove()
  588. gNode.selectAll('circle.end').remove()
  589. },
  590. zoomIn(coordinate) {
  591. if (!Array.isArray(coordinate) || coordinate.length !== 2) {
  592. return
  593. }
  594. svgNode.transition().duration(1000).call(
  595. zoomObj.transform,
  596. d3.zoomIdentity.translate(svgWidth / 2, svgHeight / 2).scale(100 / pxPerUnitLength).translate(- coordinate[0], - coordinate[1])
  597. );
  598. },
  599. zoomInToStartPoint() {
  600. this.zoomIn(startPoint)
  601. },
  602. zoomInToEndPoint() {
  603. this.zoomIn(endPoint)
  604. },
  605. resetZoom() {
  606. svgNode.transition().duration(1000).call(
  607. zoomObj.transform,
  608. d3.zoomIdentity.scale(1)
  609. )
  610. },
  611. onSetHeightFilter() {
  612. gNode.selectAll('rect').attr('visibility', (d) => {
  613. if ((this.formData.maxHeight !== '') && (this.formData.maxHeight < d[2])) {
  614. return 'hidden'
  615. } else if ((this.formData.minHeight !== '') && (this.formData.minHeight > d[2])) {
  616. return 'hidden'
  617. } else {
  618. return 'visible'
  619. }
  620. })
  621. },
  622. onResetHeightFilter() {
  623. this.formData.maxHeight = ''
  624. this.formData.minHeight = ''
  625. gNode.selectAll('rect').attr('visibility', (d) => {
  626. return 'visible'
  627. })
  628. },
  629. onAddPointUndo() {
  630. },
  631. onAddPointRedo() {
  632. },
  633. onAddPoint() {
  634. // 解析svg的transform信息
  635. let translateX = 0
  636. let translateY = 0
  637. let scale = 1
  638. const gNodetransformStr = gNode.attr('transform')
  639. if (gNodetransformStr) {
  640. const translateStr = gNodetransformStr.split(' ')[0]
  641. const translateRegex = /translate\(([^,]+),([^,]+)\)/;
  642. const translateMatch = translateStr.match(translateRegex);
  643. const scaleStr = gNodetransformStr.split(' ')[1]
  644. const scaleRegex = /scale\(([^)]+)\)/;
  645. const scaleMatch = scaleStr.match(scaleRegex);
  646. translateX = Number(translateMatch[1]);
  647. translateY = Number(translateMatch[2]);
  648. scale = Number(scaleMatch[1]);
  649. }
  650. console.log('transform info: ', translateX, translateY, scale);
  651. // 选择框位置复原到svg transfrom前
  652. const brushLeftPxBeformTransform = (brushLeftPx - translateX) / scale
  653. const brushTopPxBeformTransform = (brushTopPx - translateY) / scale
  654. const brushRightPxBeformTransform = (brushRightPx - translateX) / scale
  655. const brushBottomPxBeformTransform = (brushBottomPx - translateY) / scale
  656. // 选择框位置复原到原始坐标系
  657. const brushLeft = (brushLeftPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
  658. const brushTop = (brushTopPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
  659. const brushRight = (brushRightPxBeformTransform - svgWidth / 2) / pxPerUnitLength + xCenter
  660. const brushBottom = (brushBottomPxBeformTransform - svgHeight / 2) / pxPerUnitLength + yCenter
  661. console.log('brush area in raw coordinate: ', brushLeft, brushTop, brushRight, brushBottom);
  662. // 计算出选择框的外围影响区域
  663. const affectionAreaLeft = brushLeft - pointDistance * Math.SQRT2 * 1.25
  664. const affectionAreaTop = brushTop - pointDistance * Math.SQRT2 * 1.25
  665. const affectionAreaRight = brushRight + pointDistance * Math.SQRT2 * 1.25
  666. const affectionAreaBottom = brushBottom + pointDistance * Math.SQRT2 * 1.25
  667. console.log('affection area in raw coordinate: ', affectionAreaLeft, affectionAreaTop, affectionAreaRight, affectionAreaBottom);
  668. // 筛选出框选区域可能影响到的所有外围点
  669. // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
  670. // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  671. const affectedPointList = []
  672. for (const point of rawWholeData) {
  673. // 如果z不合格,pass
  674. if (Math.abs(point.z - this.formData.addPointHeight) > this.formData.connectionMaxHeightGap) {
  675. continue
  676. }
  677. // 如果在affection范围外,pass
  678. if (point.x < affectionAreaLeft || point.x > affectionAreaRight || point.y < affectionAreaTop || point.y > affectionAreaBottom) {
  679. continue
  680. }
  681. // 如果在框选区域内,pass
  682. if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
  683. continue
  684. }
  685. affectedPointList.push(point)
  686. }
  687. // todo: 如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  688. console.log('affectedPointList: ', affectedPointList);
  689. // 筛选出框选区域内所有已存在的点
  690. // 筛选规则:除了x、y坐标,还要看z坐标是否在当前连通规则给定的高度区间。
  691. // 筛选后,如果各已存在的点之间有重叠的,则报错,要求调整可判定连通的最大高度差。
  692. const pointInBrushList = []
  693. for (const point of rawWholeData) {
  694. // 如果z不合格,pass
  695. if (Math.abs(point.z - this.formData.addPointHeight) > this.formData.connectionMaxHeightGap) {
  696. continue
  697. }
  698. // 如果在框选区域内,留下
  699. if (point.x >= brushLeft && point.x <= brushRight && point.y >= brushTop && point.y <= brushBottom) {
  700. pointInBrushList.push(point)
  701. }
  702. }
  703. gNode.selectAll('rect')
  704. .attr('fill', (d) => {
  705. if (affectedPointList.find((affectedPoint) => {
  706. // console.log(affectedPoint.id, d[4]);
  707. return affectedPoint.id === d[4]
  708. })) {
  709. return 'blue'
  710. } else if (pointInBrushList.find((pointInBrush) => {
  711. return pointInBrush.id === d[4]
  712. })) {
  713. return 'green'
  714. } else {
  715. return `black`
  716. }
  717. })
  718. if (!affectedPointList.length) {
  719. window.alert('请在已有点位附近新增点位。')
  720. return
  721. }
  722. if (!pointInBrushList.length) {
  723. window.alert('请保证框选区域内至少已有一个点位。')
  724. return
  725. }
  726. let activePointIdx = 0
  727. let newPointId = rawWholeData[rawWholeData.length - 1].id + 1
  728. // 位于框选区域内的所有点位构成稀疏图模型,用邻接表表示,依次处理其顶点表中各顶点。
  729. while(activePointIdx <= pointInBrushList.length - 1) {
  730. // 拿到当前顶点
  731. const activePoint = pointInBrushList[activePointIdx]
  732. console.log('顶点表:', pointInBrushList);
  733. console.log('当前处理顶点Idx:', activePointIdx);
  734. console.log('当前处理顶点:', activePoint);
  735. // 当前顶点有可能是本次补点前就存在的,所以清空其邻居信息
  736. activePoint.ids = []
  737. const tempIds1 = [] // 临时记录上下左右邻居
  738. const tempIds2 = [] // 临时记录斜角邻居
  739. // 拿到当前顶点的8个相邻点位
  740. const neighbourPosList = getNeighbourLocations(pointInBrushList[activePointIdx], pointDistance, rowSlope)
  741. // 处理8个相邻点位
  742. for (const key in neighbourPosList) {
  743. if (Object.hasOwnProperty.call(neighbourPosList, key)) {
  744. // 看看这个相邻点位属于上下左右邻居还是斜角邻居。
  745. let neighbourType = 0
  746. if (['posRight', 'posBottom', 'posLeft', 'posTop'].incldes(key)) {
  747. neighbourType = 1
  748. } else {
  749. neighbourType = 2
  750. }
  751. const neiPos = neighbourPosList[key];
  752. // 如果点位在框选区域外
  753. if (neiPos.x < brushLeft || neiPos.x > brushRight || neiPos.y < brushTop || neiPos.y > brushBottom) {
  754. // 在外围点位表中找匹配的点
  755. const matchedPoint = affectedPointList.find((affectedPoint) => {
  756. return getDistance2D(affectedPoint, neiPos) < pointDistance * 0.1
  757. })
  758. // 如果找到了匹配点
  759. if (matchedPoint) {
  760. // 记录自己与其关系。
  761. if (neighbourType === 1) {
  762. tempIds1.push(matchedPoint.id)
  763. } else {
  764. tempIds2.push(matchedPoint.id)
  765. }
  766. // 酌情修改这个外围点位与自己的关系。
  767. // 相邻点位数组ids中,前4项表示上下左右邻居,后四项表示斜角邻居。
  768. let idsStartIdx = -1
  769. let idsEndIdx = -1
  770. if (neighbourType === 1) {
  771. idsStartIdx = 0
  772. idsEndIdx = 4
  773. } else {
  774. idsStartIdx = 4
  775. idsEndIdx = 8
  776. }
  777. if (!matchedPoint.ids.slice(idsStartIdx, idsEndIdx).find((id) => {
  778. return id === activePoint.id
  779. })) {
  780. const emptyIdx = matchedPoint.ids.slice(idsStartIdx, idsEndIdx).findIndex((id) => {
  781. return id === '-1'
  782. })
  783. console.assert(emptyIdx !== -1, 'emptyIdx居然不存在?!')
  784. matchedPoint.ids[idsStartIdx + emptyIdx] = activePoint.id
  785. }
  786. }
  787. // 如果没找到匹配点
  788. else {
  789. // 记录自己与其关系。
  790. if (neighbourType === 1) {
  791. tempIds1.push('-1')
  792. } else {
  793. tempIds2.push('-1')
  794. }
  795. }
  796. }
  797. // 如果点位在框选区域内
  798. else {
  799. // 在顶点表中找匹配的点
  800. const matchedPoint = pointInBrushList.find((pointInBrush) => {
  801. return getDistance2D(pointInBrush, neiPos) < pointDistance * 0.1
  802. })
  803. // 如果找到了匹配点
  804. if (matchedPoint) {
  805. // 记录自己与其关系。
  806. if (neighbourType === 1) {
  807. tempIds1.push(matchedPoint.id)
  808. } else {
  809. tempIds2.push(matchedPoint.id)
  810. }
  811. }
  812. // 如果没找到匹配点
  813. else {
  814. // 创建新点位,加入顶点表
  815. pointInBrushList.push({
  816. id: newPointId,
  817. x: neiPos.x,
  818. y: neiPos.y,
  819. z: this.formData.addPointHeight,
  820. })
  821. newPointId++
  822. // 记录自己与其关系
  823. if (neighbourType === 1) {
  824. tempIds1.push(String(newPointId))
  825. } else {
  826. tempIds2.push(String(newPointId))
  827. }
  828. }
  829. }
  830. }
  831. } // end of 处理8个相邻点位
  832. activePointIdx++
  833. } // end of 依次处理顶点表中各顶点
  834. },
  835. },
  836. }
  837. </script>
  838. <style>
  839. #app {
  840. font-family: Avenir, Helvetica, Arial, sans-serif;
  841. -webkit-font-smoothing: antialiased;
  842. -moz-osx-font-smoothing: grayscale;
  843. color: #2c3e50;
  844. }
  845. .formsWrapper {
  846. display: flex;
  847. align-items: center;
  848. justify-content: space-between;
  849. }
  850. .formsWrapper .form1 {
  851. width: 25vw;
  852. }
  853. .formsWrapper .form2 {
  854. width: 45vw;
  855. }
  856. .formsWrapper .form3 {
  857. width: 25vw;
  858. }
  859. .infoText {
  860. min-height: 2em;
  861. }
  862. .map {
  863. display: flex;
  864. align-items: center;
  865. }
  866. .map > .map-control-area {
  867. margin-left: 20px;
  868. }
  869. .map > .map-control-area > .panel {
  870. border: 1px solid black;
  871. display: flex;
  872. flex-direction: column;
  873. justify-content: center;
  874. align-items: center;
  875. margin-bottom: 15px;
  876. }
  877. .map > .map-control-area > .panel > .btn {
  878. margin: 10px;
  879. }
  880. .map > .map-control-area > .panel {
  881. padding-left: 10px;
  882. padding-right: 10px;
  883. }
  884. .map > .map-control-area > .panel input{
  885. width: 80px;
  886. }
  887. .map > .map-control-area > .panel > .btn-group {
  888. display: flex;
  889. justify-content: space-between;
  890. margin-bottom: 10px;
  891. }
  892. .svgWrapper {
  893. display: inline-block;
  894. overflow: hidden;
  895. background: #eee;
  896. }
  897. </style>