123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- <template>
- <div class="formsWrapper">
- <el-form
- class="form1"
- label-position="left"
- >
- <el-form-item label="场景名或完整url">
- <el-input
- v-model="sceneNameOrUrl"
- autofocus
- />
- </el-form-item>
- <el-button
- type="primary"
- @click="getWholeData"
- >
- 重新获取全部数据点
- </el-button>
- </el-form>
- <el-form
- class="form2"
- label-position="top"
- >
- <el-form-item
- :label="`路径1数据(必填)(节点index,以英文逗号分隔)(浅蓝色大圆圈表示)(路径长度:${path1Length})`"
- >
- <el-input v-model="formData.path1" />
- </el-form-item>
- <el-form-item
- :label="`路径2数据(选填)(节点index,以英文逗号分隔)(深蓝色小圆圈表示)(路径长度:${path2Length})`"
- >
- <el-input v-model="formData.path2" />
- </el-form-item>
- <el-button
- type="primary"
- @click="renderPath"
- >
- 显示路径
- </el-button>
- <el-button @click="onResetPath">
- 清空
- </el-button>
- </el-form>
- <el-form
- class="form3"
- label-position="top"
- >
- <el-form-item label="起点坐标(以英文逗号分隔)(浅绿色小圆圈表示)">
- <el-input v-model="formData.startPoint" />
- </el-form-item>
- <el-form-item label="终点坐标(以英文逗号分隔)(深绿色小圆圈表示)">
- <el-input v-model="formData.endPoint" />
- </el-form-item>
- <el-button
- type="primary"
- @click="renderStartEndPoint"
- >
- 显示起点终点
- </el-button>
- <el-button @click="onResetStartEndPoint">
- 清空
- </el-button>
- </el-form>
- </div>
- <div class="infoText">
- {{ infoText }}
- </div>
- <div class="map">
- <div class="svgWrapper" />
- <div class="map-control-area">
- <div class="zoom-control panel">
- 聚焦操作
- <el-button
- class="btn"
- type="primary"
- @click="zoomInToStartPoint"
- >
- 聚焦到起点
- </el-button>
- <el-button
- class="btn"
- type="primary"
- @click="zoomInToEndPoint"
- >
- 聚焦到终点
- </el-button>
- <el-button
- class="btn"
- @click="resetZoom"
- >
- 取消聚焦
- </el-button>
- </div>
- <el-form
- class="height-filter panel"
- label-position="top"
- >
- 高度筛选
- <el-form-item
- :label="`高度上限`"
- >
- <el-input
- v-model="formData.maxHeight"
- type="number"
- />
- </el-form-item>
- <el-form-item
- :label="`高度下限`"
- >
- <el-input
- v-model="formData.minHeight"
- type="number"
- />
- </el-form-item>
- <div class="btn-group">
- <el-button
- type="primary"
- @click="onSetHeightFilter"
- >
- 确定
- </el-button>
- <el-button @click="onResetHeightFilter">
- 清空
- </el-button>
- </div>
- </el-form>
- </div>
- </div>
- </template>
- <script>
- import * as d3 from "d3";
- import { getWholeData } from "./api.js";
- import { ElLoading } from 'element-plus'
- const LENGTH_PER_POINT = 0.36
- // 视口尺寸
- let svgWidth = document.documentElement.clientWidth - 200
- let svgHeight = document.documentElement.clientHeight - 280
- let svgRatio = svgWidth / svgHeight
- let pxPerUnitLength = 0 // 原始单位长度对应的像素数
- let rawWholeData = []
- let wholeDataForRender = []
- let startPoint = null
- let endPoint = null
- // 全部数据点的分布中心
- let xCenter = 0
- let yCenter = 0
- function resetGlobalVars() {
- pxPerUnitLength = 0 // 原始单位长度对应的像素数
- rawWholeData = []
- wholeDataForRender = []
- xCenter = 0
- yCenter = 0
- startPoint = null
- endPoint = null
- }
- let svgNode = null
- let gNode = null
- let zoomObj = null
- export default {
- name: 'App',
- data() {
- return {
- sceneNameOrUrl: 'SS-t-NZUICC2fRLi',
- infoText: '',
- loadingHandler: null,
- formData: {
- 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',
- path2: '',
- startPoint: '-38.5, 10.8',
- endPoint: '-28.4, 12.2',
- maxHeight: '',
- minHeight: '',
- }
- }
- },
- computed: {
- path1Length() {
- return this.getInputPathLength(this.formData.path1)
- },
- path2Length() {
- return this.getInputPathLength(this.formData.path2)
- },
- },
- mounted() {
- svgNode = d3.select('.svgWrapper').append("svg")
- .attr("width", svgWidth)
- .attr('height', svgHeight)
- gNode = svgNode.append('g')
- // this.getWholeData()
- },
- methods: {
- getInputPathLength(input) {
- let temp = input.trim()
- if(temp[0] === '[') { temp = temp.substr(1)}
- if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
- temp = temp.trim()
- if (!temp) {
- return 0
- }
- let mapSuccess = true
- temp = temp.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- const num = Number(item)
- if (!Number.isSafeInteger(num)) {
- mapSuccess = false
- return undefined
- } else {
- return num
- }
- }
- })
- if (!mapSuccess) {
- return '?'
- } else {
- return temp.filter((item) => {
- return item !== undefined
- }).length
- }
- },
- inputPathStringToArray(input) {
- let temp = input.trim()
- if(temp[0] === '[') { temp = temp.substr(1)}
- if (temp[temp.length - 1] === ']') { temp = temp.substr(0, temp.length - 1) }
- temp = temp.trim()
- if (!temp) {
- return []
- }
- let mapSuccess = true
- temp = temp.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- const num = Number(item)
- if (!Number.isSafeInteger(num)) {
- mapSuccess = false
- return undefined
- } else {
- return num
- }
- }
- })
- if (!mapSuccess) {
- window.alert(`解析输入路径失败:${input}`)
- return -1
- } else {
- return temp.filter((item) => {
- return item !== undefined
- })
- }
- },
- getWholeData() {
- if (!this.sceneNameOrUrl.trim()) {
- window.alert('场景名或完整url必填!')
- return
- }
- this.loadingHandler = ElLoading.service({
- lock: true,
- text: 'Loading',
- background: 'rgba(0, 0, 0, 0.7)',
- })
- resetGlobalVars()
- gNode.selectAll('rect').remove()
- gNode.selectAll('circle').remove()
-
- const that = this
- getWholeData(this.sceneNameOrUrl).then((res) => {
- rawWholeData = res
- // 所有点的分布情况
- let xArray = rawWholeData.map((eachPoint) => {
- return eachPoint.x
- })
- let xLength = Math.max(...xArray) - Math.min(...xArray)
- xCenter = (Math.max(...xArray) + Math.min(...xArray)) / 2
- let yArray = rawWholeData.map((eachPoint) => {
- return eachPoint.y
- })
- let yLength = Math.max(...yArray) - Math.min(...yArray)
- yCenter = (Math.max(...yArray) + Math.min(...yArray)) / 2
- let zArray = rawWholeData.map((eachPoint) => {
- return eachPoint.z
- })
- let zLength = Math.max(...zArray) - Math.min(...zArray)
- let zMin = Math.min(...zArray)
- let areaRatio = xLength / yLength
- // 各个点坐标映射到视口坐标
- if (svgRatio >= areaRatio) { // 分布范围应略小于svg尺寸
- pxPerUnitLength = svgHeight / yLength * 0.9
- } else {
- pxPerUnitLength = svgWidth / xLength * 0.9
- }
- let wholeXArrayInPx = xArray.map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let wholeYArrayInPx = yArray.map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- // 组合成最终数据用来渲染
- for (let index = 0; index < rawWholeData.length; index++) {
- console.assert(rawWholeData[index].id === (index + 1), '数据点id和数据点在数组中的位置不相符!')
- wholeDataForRender.push([wholeXArrayInPx[index], wholeYArrayInPx[index], zArray[index], JSON.stringify(rawWholeData[index]), rawWholeData[index].id])
- }
- gNode.selectAll('rect').data(wholeDataForRender).enter().append('rect')
- .attr('x', (d) => d[0] - LENGTH_PER_POINT * pxPerUnitLength / 2)
- .attr('y', (d) => d[1] - LENGTH_PER_POINT * pxPerUnitLength / 2)
- .attr('width', LENGTH_PER_POINT * pxPerUnitLength)
- .attr('height', LENGTH_PER_POINT * pxPerUnitLength)
- .attr('fill', (d) => {
- return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
- })
- .attr('render-data', (d) => {
- return d
- })
- gNode.selectAll('rect').on('mouseover', function(e) {
- d3.select(this).attr('fill', 'orange')
- let renderDataItem = e.target.attributes['render-data'].value
- let renderDataItemArray = renderDataItem.split(',')
- that.infoText = `数据点id: ${renderDataItemArray[renderDataItemArray.length - 1]}, \n具体值: ${renderDataItem.match(/^[^{]+(\{.+\})[^}]+$/)[1]}`
- }).on('mouseleave', function (e) {
- d3.select(this).attr('fill', (d) => {
- return `rgba(${Math.round((d[2] -zMin) / zLength * 255)}, 0, 0, 1)`
- })
- that.infoText = ''
- })
- zoomObj = d3.zoom().on("zoom", zoomed)
- svgNode.call(zoomObj);
- function zoomed({transform}) {
- gNode.attr("transform", transform);
- }
- }).finally(() => {
- this.loadingHandler.close()
- })
- },
- renderPath() {
- if (!this.formData.path1.trim()) {
- window.alert('路径1必填!')
- return
- }
-
- gNode.selectAll('circle.path1').remove()
- gNode.selectAll('circle.path2').remove()
-
-
- let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
- if (rawPathDataIndex === -1) {
- return
- }
- let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
- if (rawPathDataIndex2 === -1) {
- return
- }
- // 基于path index 拿到path节点数组
- let rawPathData = []
- for (let index = 0; index < rawPathDataIndex.length; index++) {
- const element = rawWholeData[rawPathDataIndex[index] - 1];
- // 假设节点id和节点在数组中出现顺序相符
- console.assert(element.id === rawPathDataIndex[index], '按照id寻找路径节点失败!')
- rawPathData.push(element)
- }
- let rawPathData2 = []
- for (let index = 0; index < rawPathDataIndex2.length; index++) {
- const element = rawWholeData[rawPathDataIndex2[index] - 1];
- // 假设节点id和节点在数组中出现顺序相符
- console.assert(element.id === rawPathDataIndex2[index], '按照id寻找路径节点失败!')
- rawPathData2.push(element)
- }
- // 各个点坐标映射到视口坐标
- let pathXArrayInPx = rawPathData.map((eachPoint) => {
- return eachPoint.x
- }).map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let pathYArrayInPx = rawPathData.map((eachPoint) => {
- return eachPoint.y
- }).map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- let pathXArrayInPx2 = rawPathData2.map((eachPoint) => {
- return eachPoint.x
- }).map((eachX) => {
- return (eachX - xCenter) * pxPerUnitLength + svgWidth / 2
- })
- let pathYArrayInPx2 = rawPathData2.map((eachPoint) => {
- return eachPoint.y
- }).map((eachY) => {
- return (eachY - yCenter) * pxPerUnitLength + svgHeight / 2
- })
- // 组合成最终数据用来渲染
- let pathDataForRender = []
- for (let index = 0; index < rawPathData.length; index++) {
- pathDataForRender.push([pathXArrayInPx[index], pathYArrayInPx[index]])
- }
- let pathDataForRender2 = []
- for (let index = 0; index < rawPathData2.length; index++) {
- pathDataForRender2.push([pathXArrayInPx2[index], pathYArrayInPx2[index]])
- }
- // 进行渲染
- gNode.selectAll('circle.path1').data(pathDataForRender).enter().append('circle')
- .classed('path1', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 2)
- .attr('fill', '#000088')
- .attr('pointer-events', 'none')
- gNode.selectAll('circle.path2').data(pathDataForRender2).enter().append('circle')
- .classed('path2', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3)
- .attr('fill', 'blue')
- .attr('pointer-events', 'none')
- },
- onResetPath() {
- this.formData.path1 = ''
- this.formData.path2 = ''
- gNode.selectAll('circle.path1').remove()
- gNode.selectAll('circle.path2').remove()
- },
- renderStartEndPoint() {
- gNode.selectAll('circle.start').remove()
- gNode.selectAll('circle.end').remove()
- startPoint = this.formData.startPoint.trim()
- if(startPoint[0] === '[') { startPoint = startPoint.substr(1)}
- if (startPoint[startPoint.length - 1] === ']') { startPoint = startPoint.substr(0, startPoint.length - 1) }
- startPoint = startPoint.trim()
- startPoint = startPoint.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- return Number(item)
- }
- })
- startPoint = startPoint.filter((item) => {
- return item !== undefined
- })
- if (startPoint.length !== 2) {
- window.alert(`解析起点坐标失败`)
- startPoint = null
- }
- endPoint = this.formData.endPoint.trim()
- if(endPoint[0] === '[') { endPoint = endPoint.substr(1)}
- if (endPoint[endPoint.length - 1] === ']') { endPoint = endPoint.substr(0, endPoint.length - 1) }
- endPoint = endPoint.trim()
- endPoint = endPoint.split(',').map((item) => {
- item = item.trim()
- if (!item) {
- return undefined
- } else {
- return Number(item)
- }
- })
- endPoint = endPoint.filter((item) => {
- return item !== undefined
- })
- if (endPoint.length !== 2) {
- window.alert(`解析终点坐标失败`)
- endPoint = null
- }
-
- if (startPoint) {
- // 起点坐标映射到视口坐标
- startPoint[0] = (startPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
- startPoint[1] = (startPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
- // 进行渲染
- gNode.selectAll('circle.start').data([startPoint]).enter().append('circle')
- .classed('start', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3.5)
- .attr('fill', 'rgba(50, 255, 50, 1)')
- .attr('pointer-events', 'none')
- }
- if (endPoint) {
- // 终点坐标映射到视口坐标
- endPoint[0] = (endPoint[0] - xCenter) * pxPerUnitLength + svgWidth / 2
- endPoint[1] = (endPoint[1] - yCenter) * pxPerUnitLength + svgHeight / 2
- // 进行渲染
- gNode.selectAll('circle.end').data([endPoint]).enter().append('circle')
- .classed('end', true)
- .attr('cx', (d) => d[0])
- .attr('cy', (d) => d[1])
- .attr('r', LENGTH_PER_POINT * pxPerUnitLength / 3.5)
- .attr('fill', 'green')
- .attr('pointer-events', 'none')
- }
- },
- onResetStartEndPoint() {
- this.formData.startPoint = ''
- this.formData.endPoint = ''
- gNode.selectAll('circle.start').remove()
- gNode.selectAll('circle.end').remove()
- },
- zoomIn(coordinate) {
- if (!Array.isArray(coordinate) || coordinate.length !== 2) {
- return
- }
- svgNode.transition().duration(1000).call(
- zoomObj.transform,
- d3.zoomIdentity.translate(svgWidth / 2, svgHeight / 2).scale(100 / pxPerUnitLength).translate(- coordinate[0], - coordinate[1])
- );
- },
- zoomInToStartPoint() {
- this.zoomIn(startPoint)
- },
- zoomInToEndPoint() {
- this.zoomIn(endPoint)
- },
- resetZoom() {
- svgNode.transition().duration(1000).call(
- zoomObj.transform,
- d3.zoomIdentity.scale(1)
- )
- },
- onSetHeightFilter() {
- gNode.selectAll('rect').attr('visibility', (d) => {
- if ((this.formData.maxHeight !== '') && (this.formData.maxHeight < d[2])) {
- return 'hidden'
- } else if ((this.formData.minHeight !== '') && (this.formData.minHeight > d[2])) {
- return 'hidden'
- } else {
- return 'visible'
- }
- })
- },
- onResetHeightFilter() {
- this.formData.maxHeight = ''
- this.formData.minHeight = ''
- gNode.selectAll('rect').attr('visibility', (d) => {
- return 'visible'
- })
- }
- },
- }
- </script>
- <style>
- #app {
- font-family: Avenir, Helvetica, Arial, sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- color: #2c3e50;
- }
- .formsWrapper {
- display: flex;
- align-items: center;
- justify-content: space-between;
- }
- .formsWrapper .form1 {
- width: 25vw;
- }
- .formsWrapper .form2 {
- width: 45vw;
- }
- .formsWrapper .form3 {
- width: 25vw;
- }
- .infoText {
- min-height: 2em;
- }
- .map {
- display: flex;
- align-items: center;
- }
- .map > .map-control-area {
- margin-left: 20px;
- }
- .map > .map-control-area > .panel {
- border: 1px solid black;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- margin-bottom: 15px;
- }
- .map > .map-control-area > .panel > .btn {
- margin: 10px;
- }
- .map > .map-control-area > .panel.height-filter {
- padding-left: 10px;
- padding-right: 10px;
- }
- .map > .map-control-area > .panel.height-filter > .btn-group {
- display: flex;
- justify-content: space-between;
- margin-bottom: 10px;
- }
- .svgWrapper {
- display: inline-block;
- overflow: hidden;
- background: #eee;
- }
- </style>
|