Pārlūkot izejas kodu

起点终点坐标输入,一键自动缩放

任一存 3 gadi atpakaļ
vecāks
revīzija
16c1c21693
2 mainītis faili ar 263 papildinājumiem un 60 dzēšanām
  1. 262 59
      src/App.vue
  2. 1 1
      src/api.js

+ 262 - 59
src/App.vue

@@ -1,39 +1,89 @@
 <template>
-  <el-form label-position="left">
-    <el-form-item label="场景名或完整url">
-      <el-input v-model="sceneNameOrUrl" />
-    </el-form-item>
-    <el-button
-      type="primary"
-      @click="getWholeData"
+  <div class="formsWrapper">
+    <el-form
+      class="form1"
+      label-position="left"
     >
-      重新获取全部数据点
-    </el-button>
-  </el-form>
-
-  <el-form label-position="top">
-    <el-form-item label="路径1数据(必填)(在全部节点中的index,以英文逗号分隔)(蓝色圆圈表示)">
-      <el-input v-model="formData.path1" />
-    </el-form-item>
-    <el-form-item label="路径2数据(选填)(在全部节点中的index,以英文逗号分隔)(绿色数字1表示)">
-      <el-input v-model="formData.path2" />
-    </el-form-item>
-
-    <el-button
-      type="primary"
-      @click="renderPath"
+      <el-form-item label="场景名或完整url">
+        <el-input v-model="sceneNameOrUrl" />
+      </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,以英文逗号分隔)(浅蓝色大圆圈表示)">
+        <el-input v-model="formData.path1" />
+      </el-form-item>
+      <el-form-item label="路径2数据(选填)(在全部节点中的index,以英文逗号分隔)(深蓝色小圆圈表示)">
+        <el-input v-model="formData.path2" />
+      </el-form-item>
+      <el-button
+        type="primary"
+        @click="renderPath"
+      >
+        显示路径
+      </el-button>
+      <el-button @click="onResetPath">
+        Reset
+      </el-button>
+    </el-form>
+    <el-form
+      class="form3"
+      label-position="top"
     >
-      显示路径
-    </el-button>
-    <el-button @click="resetForm">
-      Reset
-    </el-button>
-  </el-form>
+      <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">
+        Reset
+      </el-button>
+    </el-form>
+  </div>
 
   <div class="infoText">
     {{ infoText }}
   </div>
-  <div class="svgWrapper" />
+  <div class="map">
+    <div class="svgWrapper" />
+    <div class="button-group">
+      <el-button
+        class="btn"
+        type="primary"
+        @click="zoomInToStartPoint"
+      >
+        zoom in到起点
+      </el-button>
+      <el-button
+        class="btn"
+        type="primary"
+        @click="zoomInToEndPoint"
+      >
+        zoom in到终点
+      </el-button>
+      <el-button
+        clas="btn"
+        @click="resetZoom"
+      >
+        reset zoom
+      </el-button>
+    </div>
+  </div>
 </template>
 
 <script>
@@ -44,13 +94,15 @@ import { ElLoading } from 'element-plus'
 const LENGTH_PER_POINT = 0.36
 
 // 视口尺寸
-let svgWidth = document.documentElement.clientWidth
-let svgHeight = document.documentElement.clientHeight - 330
+let svgWidth = document.documentElement.clientWidth - 200
+let svgHeight = document.documentElement.clientHeight - 280
 let viewportRatio = svgWidth / svgHeight
 
 let pxPerUnitLength = 0 // 原始单位长度对应的像素数
 let rawWholeData = []
 let wholeDataForRender = []
+let startPoint = null
+let endPoint = null
 
 // 全部数据点的分布中心
 let xCenter = 0 
@@ -60,12 +112,15 @@ function resetGlobalVars() {
   pxPerUnitLength = 0 // 原始单位长度对应的像素数
   rawWholeData = []
   wholeDataForRender = []
-  xCenter = 0 
-  yCenter = 0 
+  xCenter = 0
+  yCenter = 0
+  startPoint = null
+  endPoint = null
 }
 
 let svgNode = null
 let gNode = null
+let zoomObj = null
 
 export default {
   name: 'App',
@@ -77,6 +132,8 @@ export default {
       formData: {
         path1: '',
         path2: '',
+        startPoint: '',
+        endPoint: '',
       }
     }
   },
@@ -90,18 +147,37 @@ export default {
     this.getWholeData()
   },
   methods: {
-    inputToArray(input) {
+    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) => {
-        return Number(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 (temp.includes(NaN)) {
+      if (!mapSuccess) {
         window.alert(`解析输入路径失败:${input}`)
         return -1
+      } else {
+        return temp.filter((item) => {
+          return item !== undefined
+        })
       }
-      return temp
     },
     getWholeData() {
       if (!this.sceneNameOrUrl.trim()) {
@@ -117,7 +193,7 @@ export default {
       resetGlobalVars()
       gNode.selectAll('rect').remove()
       gNode.selectAll('circle').remove()
-      gNode.selectAll('text').remove()
+      
 
       const that = this
       getWholeData(this.sceneNameOrUrl).then((res) => {
@@ -144,9 +220,9 @@ export default {
         let areaRatio = xLength / yLength
 
         // 各个点坐标映射到视口坐标
-        if (viewportRatio >= areaRatio) { // 视口高度应略小于y轴方向分布幅度
+        if (viewportRatio >= areaRatio) { // 分布范围应略小于svg尺寸
           pxPerUnitLength = svgHeight / yLength * 0.9
-        } else { // 视口宽度应略小于x轴方向分布幅度
+        } else {
           pxPerUnitLength = svgWidth / xLength * 0.9
         }
         let wholeXArrayInPx = xArray.map((eachX) => {
@@ -180,12 +256,12 @@ export default {
           that.infoText = ''
         })
 
-        svgNode.call(d3.zoom().on("zoom", zoomed));
+        zoomObj = d3.zoom().on("zoom", zoomed)
+        svgNode.call(zoomObj);
 
         function zoomed({transform}) {
           gNode.attr("transform", transform);
         }
-
       }).finally(() => {
         this.loadingHandler.close()
       })
@@ -196,18 +272,18 @@ export default {
         return
       }
       
-      gNode.selectAll('circle').remove()
-      gNode.selectAll('text').remove()
+      gNode.selectAll('circle.path1').remove()
+      gNode.selectAll('circle.path2').remove()
+      
       
-      let rawPathDataIndex = this.inputToArray(this.formData.path1)
+      let rawPathDataIndex = this.inputPathStringToArray(this.formData.path1)
       if (rawPathDataIndex === -1) {
         return
       }
-      let rawPathDataIndex2 = this.inputToArray(this.formData.path2)
+      let rawPathDataIndex2 = this.inputPathStringToArray(this.formData.path2)
       if (rawPathDataIndex2 === -1) {
         return
       }
-      
       // 基于path index 拿到path节点数组
       let rawPathData = {
         data: [
@@ -267,26 +343,126 @@ export default {
       }
 
       // 进行渲染
-      gNode.selectAll('circle').data(pathDataForRender).enter().append('circle')
+      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', 'blue')
+        .attr('fill', '#000088')
         .attr('pointer-events', 'none')
-      // todo: 不用数字,改用不同颜色的图形。
-      gNode.selectAll('text').data(pathDataForRender2).enter().append('text')
-        .text('1')
-        .attr('x', (d) => d[0])
-        .attr('y', (d) => d[1] + LENGTH_PER_POINT * pxPerUnitLength / 2)
-        .attr('fill', 'green')
-        .attr('text-anchor', 'middle')
+      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')
-        .attr('font-size', LENGTH_PER_POINT * pxPerUnitLength * 10)
     },
-    resetForm() {
+    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', 'red')
+          .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)
+      )
+    }
   },
 }
 </script>
@@ -296,13 +472,40 @@ export default {
   font-family: Avenir, Helvetica, Arial, sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
-  text-align: center;
   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 .button-group {
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+}
+.map .button-group .btn {
+  margin: 10px;
+}
 .svgWrapper {
+  display: inline-block;
   overflow: hidden;
   background: #eee;
 }

+ 1 - 1
src/api.js

@@ -1,7 +1,7 @@
 import axios from "axios"
 import mockData from "../input-data/data3.js";
 export function getWholeData(sceneNameOrUrl) {
-  // return Promise.resolve(mockData.data)
+  return Promise.resolve(mockData.data)
   let url = sceneNameOrUrl.startsWith('http') ?
     sceneNameOrUrl :
     `http://192.168.0.11:8080/laser/route/${sceneNameOrUrl}/getRouteInfo`