<template>
  <div id="ElementDiscoveryTopology">

    <!-- 拓扑区域 -->
    <el-container style="margin:0; padding:0; height:100%;">

      <!-- 侧边按钮 -->
<!--      <el-aside width="40px" class="tool-aside">-->
<!--        <div class="tool-box">-->
<!--          <el-row>-->
<!--            <el-tooltip popper-class="tooltipBackground" effect="dark" :content="$t('DrawTopologyComponent.translate')" placement="left">-->
<!--              <el-button size = 'mini' type="primary" class="btn-zoom active" @click="changeToZoom" plain circle><i class="fa fa-hand-pointer-o"></i></el-button>-->
<!--            </el-tooltip>-->
<!--          </el-row>-->
<!--          <br/>-->
<!--          <el-row>-->
<!--            <el-tooltip popper-class="tooltipBackground" effect="dark" :content="$t('DrawTopologyComponent.areaSelection')" placement="left">-->
<!--              <el-button size = 'mini'  type="primary" class="btn-select" @click="changeToSelect" plain circle><i class="fa fa-square-o "></i></el-button>-->
<!--            </el-tooltip>-->
<!--          </el-row>-->
<!--          <br/>-->
<!--          <el-row>-->
<!--            <el-tooltip popper-class="tooltipBackground" effect="dark" :content="$t('DrawTopologyComponent.fullScreen')" placement="left">-->
<!--              <el-button size = 'mini' type="primary" @click="fullScreen" plain circle><i class="fa fa-arrows-alt"></i></el-button>-->
<!--            </el-tooltip>-->
<!--          </el-row>-->
<!--        </div>-->
<!--      </el-aside>-->

      <!-- 主拓扑图 -->
      <el-main style="padding:0 auto; overflow:hidden; " width="calc(100% - 40px)" height="100%">
        <el-container style="margin:0; padding:0; height:100%; position:relative;">
          <el-aside width="100%" height="100%" style="overflow:hidden">
            <svg id="SvgView" width="100%" height="100%" @click="blankAreaEvent" @contextmenu="blankAreaEvent" style="position:relative;"></svg>
          </el-aside>
        </el-container>
      </el-main>

    </el-container>

    <!-- 网元右键菜单 -->
    <v-contextmenu id="ContextMenu" :style="menuShow ? menuShowStyle : menuHideStyle">
      <v-contextmenu-item v-for="command in contextCommands" :key="command.key" @click="command.execute" :disabled="command.disabled">{{command.name}}</v-contextmenu-item>
    </v-contextmenu>

  </div>
</template>

<script>
import ElementDiscoveryCommands from '../../commands/ElementDiscoveryCommands'
import HelperUtil from '../../utils/HelperUtil'
import {EventBus} from '../../domain/EventBus'
import * as d3 from 'd3'

export default {
  name: 'ElementDiscoveryTopology',

  props: {
    drawGraphSizeConfig: Object, // 画图的基本规格配置
    discoveryCollection: Object, // 网元发现的集合
    selectDevicesList: Array, // 选择的多个设备
    selectLinksList: Array // 选择的多个链路
  },

  data () {
    return {
      // 定位偏移<g>所用到的记录<g>的当前位置
      groupLeft: 0,
      groupTop: 0,
      scaleK: 1,

      contextCommands: [], // 右键菜单操作命令
      signDevicesSet: new Set(), // 标记的多个设备

      menuTop: -1, // 右键菜单的上边距
      menuLeft: -1, // 右键菜单的侧边距
      menuShow: false, // 右键菜单的提示
      menuHideStyle: { // 右键菜单隐藏的样式
        display: 'none'
      },
      menuShowStyle: null, // 右键菜单显示的样式

      currentDragKey: '' // 当前拖动的设备的key
    }
  },

  mounted () {
    // 添加对EventBus事件的监听
    this.addEventBusEvents()
    // 创建SVG画布、创建视图群组g、创建标识框
    this.createSvg()
    // 为SVG添加缩放
    this.addZoomToSvg()
    // 为SVG添加刷子
    this.addBrushToSvg()
  },

  computed: {
    // 已选择的设备集合
    selectDevicesSet () {
      return new Set(this.selectDevicesList)
    },
    // 已选择的链路集合
    selectLinksSet () {
      return new Set(this.selectLinksList)
    },
    // 已有的网元
    existingDevicesList () {
      return this.discoveryCollection
        ? JSON.parse(JSON.stringify(this.discoveryCollection.devicesInDb))
        : []
    },
    // 已有的链路
    existingLinksList () {
      return this.discoveryCollection
        ? JSON.parse(JSON.stringify(this.discoveryCollection.linksInDb))
        : []
    },
    // 已有网元的IP
    existingDevicesIpSet () {
      const ipList = this.existingDevicesList.map(e => e.ipAddress)
      return new Set(ipList)
    },
    // 新发现的网元
    newDevicesList () {
      return this.discoveryCollection
        ? JSON.parse(JSON.stringify(this.discoveryCollection.devicesNew))
        : []
    },
    // 新发现的链路
    newLinksList () {
      return this.discoveryCollection
        ? JSON.parse(JSON.stringify(this.discoveryCollection.linksNew))
        : []
    },
    // 新发现的网元的deviceKey集合
    newDeviceKeysSet () {
      return new Set(this.newDevicesList.map(device => device.deviceKey))
    },
    // 新发现的链路的linkKey集合
    newLinkKeysSet () {
      return new Set(this.newLinksList.map(link => link.linkKey))
    },
    // 合并的网元List
    totalDevicesList () {
      return [...this.existingDevicesList, ...this.newDevicesList]
    },
    // 合并的网元Map
    totalDevicesMap () {
      return HelperUtil.arrayToObject(this.totalDevicesList, 'deviceKey')
    },
    // 合并的链路List
    totalLinksList () {
      return [...this.existingLinksList, ...this.newLinksList]
    },
    // 合并的链路Map
    totalLinksMap () {
      return HelperUtil.arrayToObject(this.totalLinksList, 'linkKey')
    },
    // 是否触发了拓扑渲染函数
    needToRenderView () {
      // 如果discoveryCollection中的checkFindIsEnd有变化，则渲染拓扑图
      return this.discoveryCollection
        ? this.discoveryCollection.checkFindIsEnd
        : false
    },
    // 处理链路数据，将每两个节点之间的多条连线放到一个一维数组，并将这些一维数组放到一个二维数组里
    formattedLinksList () {
      var arr = this.totalLinksList.filter(item => {
        return item.startDeviceKey !== item.endDeviceKey
      })
      const tmpStack = []
      for (let link of arr) {
        const {startDeviceKey, endDeviceKey} = link
        const startDevice = this.totalDevicesMap[startDeviceKey]
        const endDevice = this.totalDevicesMap[endDeviceKey]
        if (typeof (startDevice) === 'undefined' || typeof (endDevice) === 'undefined') continue
        Object.assign(link, {
          source: startDevice,
          target: endDevice
        })
        tmpStack.push([startDeviceKey, endDeviceKey, link])
      }

      const formattedLinksList = []
      const flagMap = {}
      for (let i = 0; i < tmpStack.length; i++) {
        if (flagMap[i]) continue
        const tmpList = []
        const preList = tmpStack[i]
        tmpList.push(preList[2])
        flagMap[i] = true
        for (let j = i + 1; j < tmpStack.length; j++) {
          if (flagMap[j]) continue
          if (this.isSameList(preList, tmpStack[j])) {
            tmpList.push(tmpStack[j][2])
            flagMap[j] = true
          }
        }
        formattedLinksList.push(tmpList)
      }

      return formattedLinksList
    },
    // 将链路集合转化成一个Object，key为deviceKey，value为与此deviceKey相连接的链路的集合
    deviceKeyToLinkKeys () {
      const tmpObj = {}
      this.totalLinksList.forEach(link => {
        const {startDeviceKey, endDeviceKey, linkKey} = link
        if (tmpObj[startDeviceKey]) tmpObj[startDeviceKey].add(linkKey)
        else tmpObj[startDeviceKey] = new Set([linkKey])
        if (tmpObj[endDeviceKey]) tmpObj[endDeviceKey].add(linkKey)
        else tmpObj[endDeviceKey] = new Set([linkKey])
      })
      return tmpObj
    }
  },

  watch: {
    // 检测是否需要渲染拓扑图
    needToRenderView () {
      this.renderView()
    }
  },

  methods: {
    /**
     * @description 添加对EventBus事件的监听函数
     * @author weixin
     * @date 2020/08/20
     */
    addEventBusEvents () {
      EventBus.$on('i-updated-device-check', this._updateDevicesCheck)
      EventBus.$on('i-updated-link-check', this._updateLinksCheck)
      EventBus.$on('discoveryDevice', this.handleDis)
      EventBus.$on('discoveryLinkAll', this.handleDisLink)
      EventBus.$on('discoveryDeviceClose', this.discoveryDeviceClose)
      EventBus.$on('discoveryLinkClose', this.discoveryLinkClose)
    },
    // 将列表中的设备全部变为选中状态，即图标变绿色
    _updateDevicesCheck (deviceKeysSet, checkState) {
      this.menuShow = false
      if (checkState === true) { // 勾选
        deviceKeysSet.forEach(deviceKey => {
          d3.select(`#node-image-${deviceKey}>svg`).style('fill', '#00cc00')
          this.selectDevicesSet.add(deviceKey)
        })
        this.$emit('updateSelectList', 'device', [...this.selectDevicesSet])
        // this._autoToggleLinks(deviceKeysSet, checkState)
      } else { // 取消勾选
        const _this = this
        deviceKeysSet.forEach(deviceKey => {
          d3.select(`#node-image-${deviceKey}>svg`).style('fill', function () {
            return _this.getTopoColor(d3.select(this.parentNode).datum())
          })
          this.selectDevicesSet.delete(deviceKey)
        })
        this.$emit('updateSelectList', 'device', [...this.selectDevicesSet])
        // this._autoToggleLinks(deviceKeysSet, checkState)
      }
    },
    _autoToggleLinks (deviceKeysSet, checkState) {
      // 在链路集合中找到所有startDeviceKey或endDeviceKey等于deviceKey的链路的linkKey
      const linkKeysList = []
      deviceKeysSet.forEach(deviceKey => {
        let linkKeys = this.deviceKeyToLinkKeys[deviceKey]
        if (linkKeys) linkKeysList.push([...linkKeys])
      })
      const linkKeysSet = new Set(linkKeysList.flat()) // 去重
      // 勾选状态
      if (checkState === true) {
        // 找到这些链路的两端device，判断是否存在未被勾选的新发现的网元(在此deviceKeysSet中的也算被勾选了)，如果不存在，则加入到需要自动勾选的链路列表
        const needToSignLinkKeysSet = new Set()
        linkKeysSet.forEach(linkKey => {
          const {startDeviceKey, endDeviceKey} = this.totalLinksMap[linkKey]
          if (!this.hasUnselectedNewDevice(startDeviceKey, endDeviceKey, checkState, deviceKeysSet)) needToSignLinkKeysSet.add(linkKey)
        })
        // 更新拓扑图的标记
        this._updateLinksCheck(needToSignLinkKeysSet, true)
      }
      // 取消勾选状态
      else {
        // 找到这些链路的两端device，判断是否存在未被勾选的新发现的网元(在此deviceKeysSet中的不算被勾选了)，如果存在，测加入到需要自动取消勾选的链路列表
        const needToUnsignLinkKeysSet = new Set()
        linkKeysSet.forEach(linkKey => {
          const {startDeviceKey, endDeviceKey} = this.totalLinksMap[linkKey]
          if (this.hasUnselectedNewDevice(startDeviceKey, endDeviceKey, checkState, deviceKeysSet)) needToUnsignLinkKeysSet.add(linkKey)
        })
        this._updateLinksCheck(needToUnsignLinkKeysSet, false)
      }
    },
    // 将列表中的链路全部变为选中状态，即颜色变绿色
    _updateLinksCheck (linkKeysSet, checkState) {
      this.menuShow = false
      if (checkState === true) { // 勾选
        linkKeysSet.forEach(linkKey => {
          d3.select(`#textPath${linkKey}`).style('stroke', '#00cc00')
          this.selectLinksSet.add(linkKey)
        })
        this.$emit('updateSelectList', 'link', [...this.selectLinksSet])
      } else { // 取消勾选
        const _this = this
        linkKeysSet.forEach(linkKey => {
          d3.select(`#textPath${linkKey}`).style('stroke', function () {
            return _this.getTopoColor(d3.select(this.parentNode).datum())
          })
          this.selectLinksSet.delete(linkKey)
        })
        this.$emit('updateSelectList', 'link', [...this.selectLinksSet])
      }
    },

    /**
     * @description 创建SVG画布，视图群组g，等...
     * @author weixin
     * @date 2020/07/31
     */
    createSvg () {
      // 创建SVG画布
      this.svg = d3.select('#SvgView')
        .attr('width', this.drawGraphSizeConfig.svgWidth)
        .attr('height', this.drawGraphSizeConfig.svgHeight)
      // 创建视图群组g
      this.g = this.svg.append('g').attr('id', 'ViewGroup')
      // 创建刷子group
      this.brush = this.g.append('g').attr('class', 'brush')
    },

    /**
     * @description 添加缩放事件
     * @author weixin
     * @date 2020/07/31
     */
    addZoomToSvg () {
      // 创建zoom函数，实现缩放和平移
      this.svgZoom = d3.zoom()
        .scaleExtent([0.8, 3]) // 设置缩放的范围
        .on('zoom', this._zoom) // 和drag类似，设置缩放监听事件，"start" "zoom" "end" 三个状态
      // svg调用zoom函数，并取消双击事件
      this.svg.call(this.svgZoom).on('dblclick.zoom', () => {})
    },
    // 添加缩放的缩放状态的响应函数
    _zoom () {
      // 左偏移量，就是<g>的左边界距离显示区域的距离
      this.groupLeft = d3.event.transform.x
      // 上偏移量,就是<g>的上边界距离显示区域的距离
      this.groupTop = d3.event.transform.y
      // 记录scale的倍数
      this.scaleK = d3.event.transform.k
      this.g.attr('transform', `translate(${this.groupLeft}, ${this.groupTop}) scale(${this.scaleK})`)
    },

    /**
     * @description 渲染视图的函数
     * @author weixin
     * @date 2020/07/31
     */
    renderView () {
      // 清除所有标记
      this.clearAllSigns()
      // 将链接的颜色信息添加进totalLinksSet中的每个link对象
      this.setLinksColor()
      // 将设备的坐标和图标信息添加进totalDevicesList中的每个device对象
      this.setDevicesPositionAndIcon()
      // 绘制链接
      this.drawLink()
      // 绘制节点
      this.drawNode()
    },

    /**
     * @description 清除所有标记
     * @author weixin
     * @date 2020/08/14
     */
    clearAllSigns () {
      this.menuShow = false // 隐藏设备菜单
      d3.selectAll('.CheckPaths').remove() // 将批量标注的框进行移除
      this.signDevicesSet.clear() // 清空列表
    },

    /**
     * @description 往链路列表中加入颜色信息以定义链路颜色
     *              topoType：0表示已有的链路，1表示新发现的链路
     * @author weixin
     * @date 2020/08/14
     */
    setLinksColor () {
      for (let link of this.totalLinksList) {
        if (this.newLinkKeysSet.has(link.linkKey)) link['topoType'] = 1
        else link['topoType'] = 0
      }
    },

    /**
     * @description 往设备列表中加入坐标使其平铺 以及 指定图标
     *              topoType：0表示已有的网元，1表示新发现的网元，2表示新发现的网元IP与已有的网元IP相同
     * @author weixin
     * @date 2020/08/11
     */
    setDevicesPositionAndIcon () {
      const hMax = 15 // 一行最多多少个设备
      const delta = 100 // 设备间位置偏移
      const marginTop = 70 // 顶部空余空间
      for (let index in this.totalDevicesList) {
        // 加入坐标值
        const element = this.totalDevicesList[index]
        element['verticalAxis'] = marginTop + Math.floor(index / hMax) * delta
        element['horizontalAxis'] = index % hMax * delta

        // 加入topoType
        if (!this.newDeviceKeysSet.has(element.deviceKey)) element['topoType'] = 0
        else if (this.existingDevicesIpSet.has(element['ipAddress'])) element['topoType'] = 2
        else element['topoType'] = 1
      }
    },

    /**
     * @description 判断两个List的前两个内容是否一致
     * @author weixin
     * @date 2020/08/11
     */
    isSameList (s1, s2) {
      return s1.slice(0, 2).every(i => s2.includes(i))
    },

    /**
     * @description 绘制连接
     * @author weixin
     * @date 2020/08/01
     */
    drawLink () {
      // 返回数据和DOM元素的交集
      let linkUpdate = this.g.selectAll('.link').data(this.formattedLinksList)
      // 返回多出的DOM集合并处理
      linkUpdate.exit().remove()
      // 反回多出的数据集合并处理
      const linkEnter = linkUpdate.enter().insert('g', '.node').classed('link', true)
      // 将enter合并到update里面去，使得更新包含第一次创建
      linkUpdate = linkUpdate.merge(linkEnter)
      // 为二维数组中的每一个一维数组绘制连线
      const _this = this
      linkUpdate.each(function (d) {
        _this.drawPath(d, d3.select(this))
      })
    },

    /**
     * @description 处理每两个节点之间的一组连线，可能包含多条
     * @author weixin
     * @date 2020/08/01
     */
    drawPath (d, object) {
      // 新建一个数组，存放坐标重新计算好的一组连线。
      const paths = []
      // 处理传过来的数据，重新计算好坐标组成一个新的数组，并进行后续的绘制。
      this.handleManyPath(d, paths)
      // 返回每条连线的path和data的选择集
      let path = object.selectAll('.path').data(paths, d => d.linkKey)
      // 清楚多余的DOM元素
      path.exit().remove()
      // 每条连线的path和text存放在一个g里
      const pathEnter = path.enter().append('g')
        .attr('class', 'path')
        .on('mouseover', this.handleLinkMouseover)
        .on('mouseout', this.handleLinkMouseout)

      // 为每条连线创建容器，当鼠标移入时，控制其透明度
      pathEnter.append('path')
        .attr('class', 'linkWrapper')
        .attr('id', p => `linkWrapper${p.linkKey}`)
      // 为每条连线创建path,并给该path赋一个id，提供给textpath使用
      pathEnter.append('path')
        .attr('class', 'path-data')
        .attr('id', p => `textPath${p.linkKey}`)

      // 为每条连线创建text和textPath
      pathEnter.append('text')
        .attr('class', 'path-text')
        .style('font-size', '12px')
        .style('font-family', 'Si mSun')
        .append('textPath')
        .attr('class', 'textPath')
        .attr('xlink:href', p => `#textPath${p.linkKey}`)

      // 合并第一次创建为更新
      path = path.merge(pathEnter)

      // 绘制容器
      path.select('.linkWrapper')
        .attr('d', p => this.getPathData(p))
        .style('stroke', p => this.getTopoColor(p))
        .style('stroke-width', '6')
        .style('fill', 'none')
        .style('stroke-linecap', 'round')
        .style('opacity', 0.4)
        .style('display', 'none')

      // 更新连线path，绘制连线
      path.select('.path-data')
        .attr('d', p => this.getPathData(p))
        .style('stroke', p => this.selectLinksSet.has(p.linkKey) ? '#00cc00' : this.getTopoColor(p))
        .style('stroke-width', '2')
        .style('fill', 'none')
        .style('stroke-linecap', 'round')
        .style('opacity', 1)

      // 此处更新的是textpath的属性，使得文字依附在path上
      path.select('.path-text')
        .attr('x', this.handleTopologyLinkTextX) // x指定文字最左侧坐标位置，根据连线两端节点的坐标，勾股出连线的长度l，让文字最左侧位于连线的中间，就是让x = l/2
        .attr('dy', -3)
        .attr('text-anchor', 'middle')
        .attr('rotate', this.handleTopologyLinkRotate)
        .select('.textPath')
        .text(this.handleTopologyLinkTextDirection)
    },

    /**
     * @description 根据链路的二维数组，转化为一个一维数组，同时使得相同两个设备之间的多条链路不会重合，错开显示
     * @author weixin
     * @param {list} paths 二维数组中的每一项，也就是每两个节点之间的一组连线，是一个一维数组
     * @date 2020/08/01
     */
    handleManyPath (d, paths) {
      let same = d[0]
      let source = same.startDeviceKey
      let target = same.endDeviceKey
      for (let i = 0; i < d.length; ++i) {
        let path = {}
        let sourceAxis = [d[i].source.horizontalAxis, d[i].source.verticalAxis], targetAxis = [d[i].target.horizontalAxis, d[i].target.verticalAxis]

        path.sameIndex = (i + 1)
        path.sameTotal = d.length
        path.sameTotalHalf = (path.sameTotal / 2)
        path.sameUneven = ((path.sameTotal % 2) !== 0)
        path.sameMiddleLink = ((path.sameUneven === true) && (Math.ceil(path.sameTotalHalf) === path.sameIndex))
        path.sameLowerHalf = (path.sameIndex <= path.sameTotalHalf)
        path.sameArcDirection = path.sameLowerHalf ? 0 : 1
        path.sameIndexCorrected = path.sameLowerHalf ? path.sameIndex : (path.sameIndex - Math.ceil(path.sameTotalHalf))
        path.maxSameHalf = Math.floor(d.length / 2)
        path.sourceAxis = sourceAxis
        path.targetAxis = targetAxis
        path.linkKey = d[i].linkKey
        if (d[i].startDeviceKey === target && d[i].endDeviceKey === source && path.sameTotal > 1) {
          path.sameArcDirection = path.sameArcDirection === 0 ? 1 : 0
        }

        path.linkName = d[i].linkName
        // path.status = d[i].status
        path.topoType = d[i].topoType
        // paths.push({'sourceAxis': sourceAxis, 'targetAxis': targetAxis, 'linkKey': d[i].linkKey, 'linkName': d[i].linkName, 'status': d[i].status})
        paths.push(path)
      }
    },

    /**
     * @description 处理拓扑图中连接的移入事件
     * @author weixin
     * @date 2020/08/01
     */
    handleLinkMouseover (p) {
      d3.select(`#linkWrapper${p.linkKey}`)
        .style('display', 'block')
    },

    /**
     * @description 处理拓扑图中连接的移出事件
     * @author weixin
     * @date 2020/08/01
     */
    handleLinkMouseout (p) {
      d3.select(`#linkWrapper${p.linkKey}`)
        .style('display', 'none')
    },

    /**
    * @description 处理拓扑图中连接的右键事件
    * @author weixin
    * @date 2020/08/01
    */
    handleTopologyLinkContextmenu (d) {
      d3.event.stopPropagation() // 禁止事件冒泡
      d3.event.preventDefault() // 禁止右键默认菜单

      // 获取此链路两端的deviceKey
      const {startDeviceKey, endDeviceKey} = this.totalLinksMap[d.linkKey]
      // 判断两端device有没有未被选中的新发现网元
      if (this.hasUnselectedNewDevice(startDeviceKey, endDeviceKey)) return

      this.contextCommands = ElementDiscoveryCommands.generateCommandsForLink(checkState => this._updateLinksCheck(new Set([d.linkKey]), checkState))
      this.showMenu() // 计算菜单位置并显示
    },

    /**
     * @description 判断此deviceKey是否为新发现的网元且未被选中
     * @param {string} startDeviceKey 起始设备的key
     * @param {string} endDeviceKey 结束设备的key
     * @param {bool} checkState 可选参数，用来表示targetDeviceKeys为选中的deviceKey列表还是取消选中的deviceKey列表
     * @param {Set} targetDeviceKeys 可选参数，即将加入选中列表的deviceKey们
     * @author weixin
     * @date 2020/08/21
     */
    hasUnselectedNewDevice (startDeviceKey, endDeviceKey, checkState = true, targetDeviceKeys = new Set()) {
      let isStartDeviceUnselectedNewDevice, isEndDeviceUnselectedNewDevice
      if (checkState === true) {
        // 起始网元是否为新发现且未被选中的网元
        isStartDeviceUnselectedNewDevice = !(!this.newDeviceKeysSet.has(startDeviceKey) || this.selectDevicesSet.has(startDeviceKey) || targetDeviceKeys.has(startDeviceKey))
        // 结束网元是否为新发现且未被选中的网元
        isEndDeviceUnselectedNewDevice = !(!this.newDeviceKeysSet.has(endDeviceKey) || this.selectDevicesSet.has(endDeviceKey) || targetDeviceKeys.has(endDeviceKey))
      } else {
        // 起始网元是否为新发现且未被选中的网元
        isStartDeviceUnselectedNewDevice = !(!this.newDeviceKeysSet.has(startDeviceKey) || (this.selectDevicesSet.has(startDeviceKey) && !targetDeviceKeys.has(startDeviceKey)))
        // 结束网元是否为新发现且未被选中的网元
        isEndDeviceUnselectedNewDevice = !(!this.newDeviceKeysSet.has(endDeviceKey) || (this.selectDevicesSet.has(endDeviceKey) && !targetDeviceKeys.has(endDeviceKey)))
      }
      // 以上两个至少有一个成立即为true
      return isStartDeviceUnselectedNewDevice || isEndDeviceUnselectedNewDevice
    },

    /**
     * @description 得到当前path 'd'属性的值，绘制连线。
     * @author weixin
     * @date 2020/08/01
     */
    getPathData (p) {
      // const M = `M${p.sourceAxis[0] + 16},${p.sourceAxis[1] + 16}`
      // const L = `L${p.targetAxis[0] + 16},${p.targetAxis[1] + 16}`
      // const pathData = `${M}${L}Z`
      // return pathData

      var dx = p.targetAxis[0] - p.sourceAxis[0],
        dy = p.targetAxis[1] - p.sourceAxis[1],
        dr = Math.sqrt(dx * dx + dy * dy),
        unevenCorrection = (p.sameUneven ? 0 : 0.5),
        arc = ((dr * p.maxSameHalf) / (p.sameIndexCorrected - unevenCorrection))
      if (p.sameMiddleLink) {
        arc = 0
      }
      let result = 'M' + (p.sourceAxis[0] + 16) + ',' + (p.sourceAxis[1] + 16) + 'A' + arc + ',' + arc + ' 0 0,' + p.sameArcDirection + ' ' + (p.targetAxis[0] + 16) + ',' + (p.targetAxis[1] + 16)
      return result

    },

    /**
     * @description 处理拓扑图中连接的文字方向的旋转
     * @author weixin
     * @date 2020/08/01
     */
    handleTopologyLinkRotate (p) {
      return p.sourceAxis[0] < p.targetAxis[0] ? 0 : 180
    },

    /**
     * @description 处理拓扑图中连接的文字的最左侧坐标的位置
     * @author weixin
     * @date 2020/08/01
     */
    handleTopologyLinkTextX (p) {
      // x指定文字最左侧坐标位置，根据连线两端节点的坐标，勾股出连线的长度l，让文字最左侧位于连线的中间，就是让x = l/2;
      const x = p.targetAxis[0] - p.sourceAxis[0], y = p.targetAxis[1] - p.sourceAxis[1]
      const l = Math.sqrt(x * x + y * y)
      return l / 2
    },

    /**
     * @description 处理拓扑图中连接的文字的顺序方向，保证文字正常显示
     * @author weixin
     * @date 2020/08/01
     */
    handleTopologyLinkTextDirection (p) {
      // 为进行旋转的时候，文字方向是正常；当进行旋转之后，文字就反序显示了，需要将文字转换为顺序。
      return p.sourceAxis[0] < p.targetAxis[0] ? p.linkName : p.linkName.split('').reverse().join('')
    },

    /**
     * @description 绘制节点
     * @author weixin
     * @date 2020/08/01
     */
    drawNode () {
      // 返回数据和DOM元素的交集
      let g1Update = this.g.selectAll('.node').data(this.totalDevicesList, d => d.deviceKey)

      // 返回多出的DOM集合并处理
      g1Update.exit().remove()

      // 返回多出的数据集合并处理
      const g1Enter = g1Update.enter().append('g')
        .style('cursor', 'pointer')
        .classed('node', true) // 注意，此处就是之前一直创建图标准的病因，并未给g1一个类，以至于，选择集为空，每次都是enter()一个新的集合。和上面g.selectAll一一对应。
        .on('tick', this.tick())
        .on('click', this.handleTopologyDeviceClick)

      // 调用拖拽函数
      g1Enter.call(this.addDragEvent())

      // 为新发现的网元中ipAddress与已有网元的ipAddress重复的网元添加红框
      g1Enter.append('path').attr('class', d => d.topoType === 2 ? 'node-exist-ip' : 'node-new-ip')
      // 为多出数据集合添加网元图标
      g1Enter.append('g').attr('class', 'node-image').attr('id', d => `node-image-${d.deviceKey}`)
      // 为多出的数据集合添加网元名称
      g1Enter.append('text').attr('class', 'node-text')
      // 此处是d3最牛逼的代码，v4新特性，将g1Enter合并到g1Update，使得第一次创建也属于更新里面。
      g1Update = g1Update.merge(g1Enter)

      // 更新网元图标的红框
      g1Update.select('.node-exist-ip')
        .attr('d', 'M -2 -2 h36 v36 h-36 Z')
        .style('display', 'block')

      // 使用svg图标作为网元节点
      g1Update.selectAll('.node-image').nodes().forEach(n => {
        d3.xml(this.selectDeviceImagePth(n.__data__)).mimeType('image/svg+xml').get((error, xml) => {
          if (error) throw error
          n.append(xml.documentElement)
          n.childNodes[0].setAttribute('width', this.drawGraphSizeConfig.imageWidth)
          n.childNodes[0].setAttribute('height', this.drawGraphSizeConfig.imageHeight)
          n.childNodes[0].style.fill = this.getTopoColor(n.__data__)
        })
      })

      // 更新网元名称
      g1Update.select('.node-text')
        .attr('dx', 15)
        .attr('dy', 45)
        .style('font-size', '12px')
        .style('text-anchor', 'middle')
        .style('font-family', 'SimSun')
        .text(d => d.displayName)
    },

    handleDis: function (val) {
      d3.select(`#node-image-${val}>svg`).style('fill', '#0000FF')
      for (var i = 0; i < this.totalDevicesList.length; i++) {
        if (this.totalDevicesList[i].deviceKey === val) {
          this.totalDevicesList[i].topoType = 0
          break
        }
      }
      this.$emit('updateSelectList', 'device', [])
    },
    handleDisLink: function (val) {
      d3.select(`#linkWrapper${val}`).style('stroke', '#0000FF')
      d3.select(`#textPath${val}`).style('stroke', '#0000FF')
      this.selectLinksSet.clear()
      for (var i = 0; i < this.totalLinksList.length; i++) {
        if (this.totalLinksList[i].linkKey === val) {
          this.totalLinksList[i].topoType = 0
          break
        }
      }
      this.$emit('updateSelectList', 'link', [])
    },
    discoveryDeviceClose: function (val) {
      this.$emit('updateSelectList', 'device', [])
      d3.select(`#node-image-${val}>svg`).style('fill', '#9A9A9A')
    },
    discoveryLinkClose: function (val) {
      this.$emit('updateSelectList', 'link', [])
      d3.select(`#linkWrapper${val}`).style('stroke', '#9A9A9A')
      d3.select(`#textPath${val}`).style('stroke', '#9A9A9A')
    },
    /**
     * @Description  : selectDeviceImagePth
     * @author       : ls
     * @date         : 2020/12/30 14:55
     * @param        :
     * @return       :
     */
    selectDeviceImagePth (d) {
      return `./static/${d.deviceTypeKey}.svg`
    },

    /**
     * @description 根据设备或链路的topoType得到拓扑图显示的颜色
     * @author weixin
     * @date 2020/08/15
     */
    getTopoColor (d) {
      const svgColorMap = {
        0: '#0000FF',
        1: '#9A9A9A',
        2: '#9A9A9A'
      }
      return svgColorMap[d.topoType]
    },

    /**
     * @description 处理拓扑图中设备的右键事件
     * @author weixin
     * @date 2020/08/01
     */
    handleTopologyDeviceContextmenu (d) {
      d3.event.stopPropagation() // 禁止事件冒泡
      d3.event.preventDefault() // 禁止右键默认菜单

      const {
        horizontalAxis: startXAxis, // 当前点击设备的x坐标
        verticalAxis: startYAxis, // 当前点击设备的y坐标
        deviceKey: key // 当前点击设备的key
      } = d

      // 如果当前设备不在signDevicesSet里面，则将当前添加进signDevicesSet并清除其他标记并显示当前标记并显示右键菜单
      if (!this.signDevicesSet.has(key)) {
        this.clearAllSigns()
        this.signDevicesSet.add(key)
        this.showDevicesOneByOne(startXAxis, startYAxis, key) // 标识选中的设备
      }
      this.contextCommands = ElementDiscoveryCommands.generateCommandsForDevice(checkState => this._updateDevicesCheck(this.signDevicesSet, checkState))
      this.showMenu() // 计算菜单位置并显示
    },

    /**
     * @description 计算右键菜单位置并显示
     * @author weixin
     * @date 2020/08/18
     */
    showMenu () {
      this.menuTop = d3.event.y
      this.menuLeft = d3.event.x + window.scrollX

      if (56 + this.menuTop >= window.innerHeight) {
        this.menuTop -= 56
      }
      if (78 + this.menuLeft >= window.innerWidth) {
        this.menuLeft -= 78
      }
      this.menuShowStyle = {
        left: `${this.menuLeft}px`,
        top: `${this.menuTop}px`,
        display: 'block'
      }
      this.menuShow = true
    },

    /**
     * @description 处理拓扑图中设备的点击事件
     * @author weixin
     * @date 2020/08/01
     */
    handleTopologyDeviceClick (d) {
      d3.event.stopPropagation() // 取消事件冒泡，防止触发blankAreaEvent
      this.menuShow = false
      const {
        horizontalAxis: startXAxis, // 当前点击设备的x坐标
        verticalAxis: startYAxis, // 当前点击设备的y坐标
        deviceKey: key // 当前点击设备的key
      } = d
      if (d3.event.ctrlKey) { // 返回值是一个布尔型的值，代表当前是 点击 || ctrl + 点击
        // 如果选择的设备已在集合中，则移除它并取消它的选择框；如果不在，则添加到集合，并添加选框
        if (this.signDevicesSet.has(key)) {
          this.signDevicesSet.delete(key) // 从已选设备集合中删除
          d3.select(`#CheckPaths${key}`).remove() // 框选删除
        } else {
          this.signDevicesSet.add(key) // 把选中的设备加入到已选设备集合中
          this.showDevicesOneByOne(startXAxis, startYAxis, key) // 依次标识 ctrl+click 选中的设备
        }
      } else { // 当前点击设备的响应
        d3.selectAll('.CheckPaths').remove() // 将批量标注的框进行移除
        this.signDevicesSet.clear() // 清空selectDeviceList
        this.signDevicesSet.add(key)
        this.showDevicesOneByOne(startXAxis, startYAxis, key) // 标识选中的设备
      }
    },

    /**
    * @description 增加拖拽事件
    * @author weixin
    * @date 2020/08/02
    */
    addDragEvent () {
      return d3.drag()
        .on('start', this.dragStart)
        .on('drag', this.dragMove)
        .on('end', this.dragEnd)
    },

    /**
     * @description 拖拽开始事件
     * @author weixin
     * @date 2020/08/02
     */
    dragStart (d) {
      this.currentDragKey = d.deviceKey // 记录下当前拖拽的设备的id
    },

    /**
     * @description 拖拽事件中拖拽行为的响应函数，并调用tick()进行位置的更新
     * @author weixin
     * @date 2020/08/02
     */
    dragMove (d) {
      d3.select(this.$el)
        .attr('transform', () => {
          d.horizontalAxis = d3.event.x
          d.verticalAxis = d3.event.y
          return `translate(${d.horizontalAxis},${d.verticalAxis})`
        })
        .on('tick', this.tick)
      this.tick()
    },

    /**
     * @description 拖拽事件结束
     * @author weixin
     * @date 2020/08/02
     */
    dragEnd (d) {
      this.currentDragKey = '' // 置空拖拽设备记录变量
    },

    /**
     * @description 添加刷子事件
     * @author weixin
     * @date 2020/07/31
     */
    addBrushToSvg () {
      this.brushFunction = d3.brush()
        .on('brush', this._brushed)
        .on('end', this._endBrushed)
    },
    // 刷子监听brush事件的响应函数
    _brushed () {
      if (this.brush.empty()) return
      // 显示刷子
      this.brush.style('opacity', 1)
    },
    // 刷子监听end事件的响应函数
    _endBrushed () {
      // 刷子覆盖区域
      const selection = d3.event.selection
      // 选择区域为空则结束
      if (!selection) return

      for (let element of this.totalDevicesList) {
        const {
          horizontalAxis: x,
          verticalAxis: y,
          deviceKey: key
        } = element
        if (selection[0][0] <= x &&
            x < selection[1][0] &&
            selection[0][1] <= y &&
            y < selection[1][1] &&
            !this.signDevicesSet.has(key)) {
          this.signDevicesSet.add(key)
          this.showDevicesOneByOne(x, y, key)
        }
      }

      this.brush.style('opacity', 0)
      d3.select('.selection').attr('width', 0).attr('height', 0)
    },

    /**
     * @description 鼠标位于空白区域的响应事件，清除一些标记
     * @author weixin
     * @date 2020/08/18
     */
    blankAreaEvent (event) {
      this.menuShow = false // 隐藏右键菜单
      if (!event.ctrlKey) { // 此时是单选设备模式
        d3.selectAll('.CheckPaths').remove() // 将批量标注的框进行移除
        this.signDevicesSet.clear() // 清空列表
      }
    },

    /**
     * @description 左侧工具栏点击刷子按钮的响应函数
     * @author weixin
     * @date 2020/07/31
     */
    changeToSelect () {
      document.getElementsByClassName('btn-select')[0].classList.add('active')
      document.getElementsByClassName('btn-zoom')[0].classList.remove('active')
      // 屏蔽缩放操作
      this.svg.on('.zoom', null)
      // 当前可视化窗口的大小以及中心点
      const {
        offsetWidth: topologySvgWidth,
        offsetHeight: topologySvgHeight
      } = document.getElementById('ElementDiscoveryTopology')
      // 将可视区域指定为刷子作用域
      const [x0, y0] = [-this.groupLeft, -this.groupTop]
      const [x1, y1] = [x0 + topologySvgWidth, y0 + topologySvgHeight]

      this.brushFunction.extent([[x0, y0], [x1, y1]]) // 设置刷子的作用域
      this.brush.call(this.brushFunction) // 调用刷子函数
      this.brush.style('display', '')
    },

    /**
     * @description 左侧工具栏点击移动按钮的响应函数
     * @author weixin
     * @date 2020/07/31
     */
    changeToZoom (e) {
      document.getElementsByClassName('btn-zoom')[0].classList.add('active')
      document.getElementsByClassName('btn-select')[0].classList.remove('active')
      this.brush.style('display', 'none') // 屏蔽刷子操作
      this.brushFunction.on('.brush', null)
      this.svg.call(this.svgZoom).on('dblclick.zoom', () => {}) // 调用缩放函数
    },

    /**
     * @description 左侧工具栏点击全屏按钮的响应函数
     * @author weixin
     * @date 2020/07/31
     */
    fullScreen () {
      // TODO: 全屏以后的缩放倍数以及左上角的点有待优化
      const element = document.getElementById('SvgView')
      const {width, height} = window.screen
      this.svg.attr('width', width)
      this.svg.attr('height', height)
      this.svg.attr('fill', '#000')
      element.requestFullscreen() ||
      element.mozRequestFullScreen() ||
      element.webkitRequestFullScreen() ||
      element.msRequestFullscreen()
    },

    /**
     * @description 创建标注框
     * @author weixin
     * @param {number} startXAxis 起始x
     * @param {number} startXAxis 起始y
     * @param {string} startXAxis key
     * @date 2020/07/31
     */
    showDevicesOneByOne (startXAxis, startYAxis, key) {
      this.g.insert('path', '.brush').attr('class', 'CheckPaths')
        .attr('id', `CheckPaths${key}`)
        .attr('d', `M${startXAxis - 2} ${startYAxis - 2} h36 v36 h-36 Z`)
    },

    /**
     * @description 位置更新函数
     * @author weixin
     * @date 2020/08/02
     */
    tick () {
      // 更新连接位置
      const _this = this
      this.g.selectAll('g.link').each(function (d) {
        _this.drawPath(d, d3.select(this))
      })

      // 更新网元及名称位置
      this.g.selectAll('.node')
        .attr('transform', d => `translate(${d.horizontalAxis},${d.verticalAxis})`)

      // 更新拖拽设备的标注框
      if (this.currentDragKey !== '') { // 当前有拖拽设备才执行以下代码
        const startXAxis = this.totalDevicesMap[this.currentDragKey].horizontalAxis
        const startYAxis = this.totalDevicesMap[this.currentDragKey].verticalAxis
        const dStr = `M${startXAxis - 2} ${startYAxis - 2} h36 v36 h-36 Z`
        this.g.select(`#CheckPaths${this.currentDragKey}`).attr('d', dStr) // 更新多选设备框
      }
    }

  }
}
</script>

<style>
/** css 属性 控制全屏时候的默认样式*/
:-webkit-full-screen {
  background: #fff;
  width: 100%;
  height: 100%;
}
/** css 属性 控制全屏时候的默认样式*/
:-moz-full-screen {
  background: #fff;
  width: 100%;
  height: 100%;
}
/** css 属性 控制全屏时候的默认样式*/
/* deeper elements */
:-webkit-full-screen #SvgView {
  width: 100%;
  height: 100%;
  background: #fff;
}

/** button 按钮的tooltip样式*/
.top {
  text-align: center;
}

.left {
  float: left;
  width: 60px;
}

.right {
  float: right;
  width: 60px;
}

.bottom {
  clear: both;
  text-align: center;
}

.left .el-tooltip__popper,
.right .el-tooltip__popper {
  padding: 8px 10px;
}

/**  自己修改提示框的背景和箭头颜色 */
.el-tooltip__popper[x-placement^=left] .popper__arrow{
  border-left-color: #5fa4e8;
}
.el-tooltip__popper[x-placement^=left] .popper__arrow:after {
  border-left-color: #5fa4e8;
}
.tooltipBackground{
  background: #5fa4e8 !important;
}
.tool-aside{
  display: flex;
  justify-content: center;
  align-items: Center;
  overflow: hidden;
  background: #f0f9eb
}
.tool-box{
  width: 100%;
  overflow: hidden;
}
.tool-box .el-button.active{
  background: #409EFF;
  border-color: #409EFF;
  color: #FFF;
}
#SvgView {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.CheckPaths {
  stroke: #dddddd;
  stroke-width: 2;
  fill: #dddddd;
}
.node-exist-ip {
  stroke: red;
  stroke-width: 2;
  fill: none;
}
</style>
