diff --git a/src/views/entityExplorer/EntityGraph.vue b/src/views/entityExplorer/EntityGraph.vue index 15807776..6bb0c7d3 100644 --- a/src/views/entityExplorer/EntityGraph.vue +++ b/src/views/entityExplorer/EntityGraph.vue @@ -3,28 +3,28 @@
+ v-model="rightBox.show" + direction="rtl" + class="entity-graph__detail" + :close-on-click-modal="true" + :modal="false" + :size="400" + :with-header="false" + destroy-on-close> @@ -74,6 +74,8 @@ export default { defaultMargin: 2, // 图像与箭头的距离 rootNode: null, isClicking: false, + isDraggingEntityNode: false, + defaultArrowSize: 3, // 箭头大小 /* 自己实现stack操作 */ stackData: { undo: [], // 后退 @@ -96,134 +98,184 @@ export default { this.rightBox.loading = false } }, - initForceGraph(initialData) { + getCoordinate (startNode, endNode) { + const startX = startNode.x + const startY = startNode.y + const endX = endNode.drawEndX + const endY = endNode.drawEndY + if (endX && startX) { + const diffVal = Math.round(Math.sqrt(Math.pow((endX - startX), 2) + Math.pow((endY - startY), 2))) + const step = 3 + const i = endNode.nextI ? endNode.nextI : 0 + if (i < diffVal / step) { + endNode.nextI = i + 1 + const dx = endX - startX + const dy = endY - startY + const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度) + const cx = startX + step * Math.cos(angle) * i + const cy = this.linearInterpolate([startX, startY], [endX, endY]).getY(cx) + return { + x: cx, + y: cy + } + } else { + return null + } + } else { + return null + } + }, + getShortenedLength (end) { + return end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理 + }, + initForceGraph (initialData) { let hoverNode = null const canvasHeight = document.body.clientHeight - 100 this.graph = ForceGraph()(document.getElementById('entityGraph')) - .height(canvasHeight) - .graphData(initialData) - .nodeCanvasObject((node, ctx) => { - /* - * 共有4种 nodeType,3种 entityType - * */ - const nodeStyle = this.getNodeStyle(node.type, node.data.entityType) - switch (node.type) { - case nodeType.rootNode: { - // 如果是鼠标点击高亮的,最外层加上第三层圆环 - if (node.id === this.clickNode.id) { - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.selectedShadowR, 0, 2 * Math.PI, false) - ctx.closePath() - ctx.fillStyle = nodeStyle.selectedShadowColor - ctx.fill() - } + .height(canvasHeight) + .graphData(initialData) + .nodeCanvasObject((node, ctx) => { + /* + * 共有4种 nodeType,3种 entityType + * */ + const nodeStyle = this.getNodeStyle(node.type, node.data.entityType) + switch (node.type) { + case nodeType.rootNode: { + // 如果是鼠标点击高亮的,最外层加上第三层圆环 + if (node.id === this.clickNode.id) { + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.selectedShadowR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = nodeStyle.selectedShadowColor + ctx.fill() + } - // 第二层圆环 + // 第二层圆环 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = node === this.clickNode || node === hoverNode + ? nodeStyle.hoveredShadowColor + : nodeStyle.shadowColor + ctx.fill() + // 内部挖空 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = nodeStyle.fillStyle + ctx.fill() + + // 第一层圆环 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.lineWidth = 1 + ctx.strokeStyle = nodeStyle.borderColor + ctx.stroke() + // 图片 + ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) + // 文字 + ctx.font = nodeStyle.font + const textWidth = ctx.measureText(node.label).width + const bckgDimensions = [textWidth, 9].map(n => n + 9 * 0.2) + ctx.fillStyle = nodeStyle.fontBgColor + ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 31, ...bckgDimensions) // 文字的白底 + + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillStyle = nodeStyle.fontColor + ctx.fillText(node.label, node.x, node.y + 30) + break + } + case nodeType.listNode: { + // 如果是鼠标点击或者悬停的,有一层圆环 + if (node === this.clickNode || node === hoverNode) { ctx.beginPath() ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false) ctx.closePath() - ctx.fillStyle = node === this.clickNode || node === hoverNode - ? nodeStyle.hoveredShadowColor - : nodeStyle.shadowColor - ctx.fill() - // 内部挖空 - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) - ctx.closePath() - ctx.fillStyle = nodeStyle.fillStyle - ctx.fill() - - // 第一层圆环 - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) - ctx.closePath() - ctx.lineWidth = 1 - ctx.strokeStyle = nodeStyle.borderColor - ctx.stroke() - // 图片 - ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) - // 文字 - ctx.font = nodeStyle.font - const textWidth = ctx.measureText(node.label).width - const bckgDimensions = [textWidth, 9].map(n => n + 9 * 0.2) - ctx.fillStyle = nodeStyle.fontBgColor - ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 31, ...bckgDimensions) // 文字的白底 - - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillStyle = nodeStyle.fontColor - ctx.fillText(node.label, node.x, node.y + 30) - break - } - case nodeType.listNode: { - // 如果是鼠标点击或者悬停的,有一层圆环 - if (node === this.clickNode || node === hoverNode) { - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false) - ctx.closePath() - if (node === this.currentSelectedNode) { - ctx.fillStyle = nodeStyle.selectedShadowColor - } else { - ctx.fillStyle = nodeStyle.hoveredShadowColor - } - ctx.fill() + if (node === this.currentSelectedNode) { + ctx.fillStyle = nodeStyle.selectedShadowColor + } else { + ctx.fillStyle = nodeStyle.hoveredShadowColor } - - // 内部填白 - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) - ctx.closePath() - ctx.fillStyle = nodeStyle.fillStyle ctx.fill() - - // 第一层圆环 - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) - ctx.closePath() - ctx.lineWidth = 1 - ctx.strokeStyle = nodeStyle.borderColor - ctx.stroke() - // 图片 - ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) - // 文字 - ctx.font = nodeStyle.font - const textWidth = ctx.measureText(node.label).width - const bckgDimensions = [textWidth, 8].map(n => n + 8 * 0.2) - ctx.fillStyle = nodeStyle.fontBgColor - ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 24, ...bckgDimensions) // 文字的白底 - - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillStyle = nodeStyle.fontColor - ctx.fillText(node.label, node.x, node.y + 24) - break } - case nodeType.entityNode: { - // 先画个白底圆环,避免 link 穿过不好看 + + // 内部填白 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = nodeStyle.fillStyle + ctx.fill() + + // 第一层圆环 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.lineWidth = 1 + ctx.strokeStyle = nodeStyle.borderColor + ctx.stroke() + // 图片 + ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) + // 文字 + ctx.font = nodeStyle.font + const textWidth = ctx.measureText(node.label).width + const bckgDimensions = [textWidth, 8].map(n => n + 8 * 0.2) + ctx.fillStyle = nodeStyle.fontBgColor + ctx.fillRect(node.x - bckgDimensions[0] / 2, node.y - bckgDimensions[1] / 2 + 24, ...bckgDimensions) // 文字的白底 + + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillStyle = nodeStyle.fontColor + ctx.fillText(node.label, node.x, node.y + 24) + break + } + case nodeType.entityNode: { + // 先画个白底圆环,避免 link 穿过不好看 + ctx.beginPath() + ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = nodeStyle.fillStyle + ctx.fill() + // 如果是鼠标点击或者悬停的,有一层圆环 + if (node === this.clickNode || node === hoverNode) { ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false) ctx.closePath() - ctx.fillStyle = nodeStyle.fillStyle - ctx.fill() - // 如果是鼠标点击或者悬停的,有一层圆环 - if (node === this.clickNode || node === hoverNode) { - ctx.beginPath() - ctx.arc(node.x, node.y, nodeStyle.shadowR, 0, 2 * Math.PI, false) - ctx.closePath() - if (node === this.clickNode) { - ctx.fillStyle = nodeStyle.selectedShadowColor - } else { - ctx.fillStyle = nodeStyle.hoveredShadowColor - } - ctx.fill() + if (node === this.clickNode) { + ctx.fillStyle = nodeStyle.selectedShadowColor + } else { + ctx.fillStyle = nodeStyle.hoveredShadowColor } - // 图片 - ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) - break + ctx.fill() } - case nodeType.tempNode: { - // 先画个白底圆环,避免 link 穿过不好看 - ctx.beginPath() + // 图片 + ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) + break + } + case nodeType.tempNode: { + // 先画个白底圆环,避免 link 穿过不好看 + ctx.beginPath() + const tempNodeDistance = this.getShortenedLength(node)// 临时节点展示动画时临时节点的初始位置距离entity节点的距离 + // 计算箭头角度 + const dx = node.x - node.sourceNode.x + const dy = node.y - node.sourceNode.y + const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度) + const startNodeX = node.sourceNode.x + tempNodeDistance * Math.cos(angle) + const startNodeY = node.sourceNode.y + tempNodeDistance * Math.sin(angle) + node.drawEndX = node.realEndX + node.drawEndY = node.realEndY + const nodeCoordinate = this.getCoordinate({ x: startNodeX, y: startNodeY }, node) + if (nodeCoordinate && !this.isDraggingEntityNode) { + ctx.arc(nodeCoordinate.x, nodeCoordinate.y, nodeStyle.innerR, 0, 2 * Math.PI, false) + ctx.closePath() + ctx.fillStyle = nodeStyle.fillStyle + ctx.fill() + // 画透明度0.7的图标 + ctx.globalAlpha = 0.7 + ctx.drawImage(node.img, nodeCoordinate.x - nodeStyle.iconWidth / 2, nodeCoordinate.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) + ctx.globalAlpha = 1 + } else { ctx.arc(node.x, node.y, nodeStyle.innerR, 0, 2 * Math.PI, false) ctx.closePath() ctx.fillStyle = nodeStyle.fillStyle @@ -232,48 +284,92 @@ export default { ctx.globalAlpha = 0.7 ctx.drawImage(node.img, node.x - nodeStyle.iconWidth / 2, node.y - nodeStyle.iconHeight / 2, nodeStyle.iconWidth, nodeStyle.iconHeight) ctx.globalAlpha = 1 - break } + break } - }) - .linkCanvasObject((link, ctx) => { - const start = link.source - const end = link.target - let width = 1 // 线宽 - const arrowSize = 3 // 箭头大小 - const shortenedLength = end.type === nodeType.rootNode ? 23 : 20 // link 末端缩短长度,root节点特殊处理 + } + }) + .linkCanvasObject((link, ctx) => { + const start = link.source + const end = link.target + let width = 1 // 线宽 + const arrowSize = this.defaultArrowSize // 箭头大小 + const shortenedLength = this.getShortenedLength(end) // link 末端缩短长度,root节点特殊处理 - // 计算箭头角度 - const dx = end.x - start.x - const dy = end.y - start.y - const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度) - const lineEndX = end.x - shortenedLength * Math.cos(angle) - const lineEndY = end.y - shortenedLength * Math.sin(angle) - const arrowEndX = lineEndX + arrowSize * Math.cos(angle) - const arrowEndY = lineEndY + arrowSize * Math.sin(angle) + // 计算箭头角度 + const dx = end.x - start.x + const dy = end.y - start.y + const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度) + const lineEndX = end.x - shortenedLength * Math.cos(angle) + const lineEndY = end.y - shortenedLength * Math.sin(angle) + const arrowEndX = lineEndX + arrowSize * Math.cos(angle) + const arrowEndY = lineEndY + arrowSize * Math.sin(angle) - // 绘制线 - let color + // 绘制线 + let color + if (link.type === linkType.temp) { + ctx.setLineDash([2, 2]) + color = 'rgba(119,131,145,0.2)' // 虚线颜色 + const startX = start.x + const startY = start.y ctx.beginPath() - if (link.type === linkType.temp) { - ctx.setLineDash([2, 2]) - color = 'rgba(119,131,145,0.2)' // 虚线颜色 - } else { - ctx.setLineDash([]) - if (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id) { - color = 'rgba(119,131,145,0.9)' // 高亮线颜色 - width = 1.2 - } else { - color = 'rgba(119,131,145,0.3)' // 普通线颜色 - } + ctx.moveTo(startX, startY) + if (end.realEndX) { + end.drawEndX = lineEndX + end.drawEndY = lineEndY } + const nodeCoordinate = this.getCoordinate(start, end) + if (nodeCoordinate && !this.isDraggingEntityNode) { + ctx.lineTo(nodeCoordinate.x, nodeCoordinate.y)// 避免出现线和箭头之间出现空白部分 + ctx.strokeStyle = color + ctx.lineWidth = width + ctx.stroke() + + ctx.save() // 保存当前状态以便之后恢复 + ctx.translate(nodeCoordinate.x + arrowSize * Math.cos(angle), nodeCoordinate.y + arrowSize * Math.sin(angle)) // 将坐标原点移动到箭头末端 + ctx.rotate(angle) // 根据链接方向旋转坐标系 + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边 + ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边 + ctx.closePath() + ctx.fillStyle = color + ctx.fill() + ctx.restore() // 恢复之前保存的状态 + } else { + ctx.lineTo(lineEndX, lineEndY)// 避免出现线和箭头之间出现空白部分 + ctx.strokeStyle = color + ctx.lineWidth = width + ctx.stroke() + + ctx.save() // 保存当前状态以便之后恢复 + ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端 + ctx.rotate(angle) // 根据链接方向旋转坐标系 + ctx.beginPath() + ctx.moveTo(0, 0) + ctx.lineTo(-arrowSize, arrowSize) // 绘制箭头的一边 + ctx.lineTo(-arrowSize, -arrowSize) // 绘制箭头的另一边 + ctx.closePath() + ctx.fillStyle = color + ctx.fill() + ctx.restore() // 恢复之前保存的状态 + } + } else { + ctx.beginPath() ctx.moveTo(start.x, start.y) + ctx.setLineDash([]) + if (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id) { + color = 'rgba(119,131,145,0.9)' // 高亮线颜色 + width = 1.2 + } else { + color = 'rgba(119,131,145,0.3)' // 普通线颜色 + } + ctx.lineTo(lineEndX, lineEndY) ctx.strokeStyle = color ctx.lineWidth = width ctx.stroke() - // 绘制箭头 ctx.save() // 保存当前状态以便之后恢复 ctx.translate(arrowEndX, arrowEndY) // 将坐标原点移动到箭头末端 ctx.rotate(angle) // 根据链接方向旋转坐标系 @@ -285,319 +381,317 @@ export default { ctx.fillStyle = color ctx.fill() ctx.restore() // 恢复之前保存的状态 - /* if (link.source.x !== undefined && link.source.y !== undefined && link.target.x !== undefined && link.target.y !== undefined) { - const nodeStyle = this.getNodeStyle(link.target.type, link.target.data.entityType) - - ctx.lineWidth = 0.5 - ctx.beginPath() - if (this.clickNode && (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id)) { - ctx.strokeStyle = link.clickColor - } else { - ctx.strokeStyle = link.color - } - - const lineDash = link.target.type === nodeType.tempNode ? [2, 2] : [] - ctx.setLineDash(lineDash) - ctx.moveTo(link.source.x, link.source.y) - const xy = this.findCircleLineIntersection(link.target.x, link.target.y, nodeStyle.innerR / 2, link.source.x, link.source.y) - ctx.lineTo(xy[0].x, xy[0].y) - this.drawArrow(ctx, link.source.x, link.source.y, xy[0].x, xy[0].y, 40, 3) - - // 圆角三角形 - if (this.clickNode && (this.clickNode.id === link.source.id || this.clickNode.id === link.target.id)) { - ctx.fillStyle = link.clickColor - } else { - ctx.fillStyle = link.color - } - - ctx.fill() - ctx.closePath() - ctx.stroke() - } */ - }) - .autoPauseRedraw(false) // keep redrawing after engine has stopped如果您有依赖于画布的不断重绘的自定义动态对象,建议关闭此选项。 - .onNodeHover(node => { - hoverNode = node || null - if (node) { - node.name = builtTooltip(node) - } - }) - .centerAt(0, 0)// 设置中心节点位置 - .zoom(0.9999) - .onNodeClick(async (node, e) => { - this.isClicking = true - this.clickNode = node || null - if (node.type !== 'tempNode') { - this.rightBox.show = true - this.cleanTempItems() - // 点击entityNode,查询数据,并根据数据生成tempNode - if (node.type === nodeType.entityNode) { - node.fx = node.x - node.fy = node.y - this.rightBox.loading = true - try { - // 若已查过数据,不重复查询 - if (!node.data.relatedEntities) { - await node.queryDetailData() + } + }) + .autoPauseRedraw(false) // keep redrawing after engine has stopped如果您有依赖于画布的不断重绘的自定义动态对象,建议关闭此选项。 + .onNodeHover(node => { + hoverNode = node || null + if (node) { + node.name = builtTooltip(node) + } + }) + .centerAt(0, 0)// 设置中心节点位置 + .zoom(0.9999) + .onNodeClick(async (node, e) => { + this.isClicking = true + this.clickNode = node || null + if (node.type !== 'tempNode') { + this.rightBox.show = true + this.cleanTempItems() + // 点击entityNode,查询数据,并根据数据生成tempNode + if (node.type === nodeType.entityNode) { + node.fx = node.x + node.fy = node.y + this.rightBox.loading = true + try { + // 若已查过数据,不重复查询 + if (!node.data.relatedEntities) { + await node.queryDetailData() + } + const toAddNodes = [] + const toAddLinks = [] + let keyCount = 0 + Object.keys(node.data.relatedEntities).forEach((k, i) => { + if (node.data.relatedEntities[k].total) { + keyCount++ } - const toAddNodes = [] - const toAddLinks = [] - const keyCount = Object.keys(node.data.relatedEntities).length - Object.keys(node.data.relatedEntities).forEach((k, i) => { - if (node.data.relatedEntities[k].total) { - // 若已有同级同类型的listNode,不生成此tempNode - const neighbors = node.getNeighbors(this.graph.graphData()) - const hasListNode = neighbors.nodes.some(b => b.data.entityType === k) - if (!hasListNode) { - const tempNode = new Node( - nodeType.tempNode, - `${node.realId}__${k}__temp`, - { - entityType: k, - ...this.generateTempNodeCoordinate(node.getSourceNode(this.graph.graphData()), e) - }, - node, - this.getIconUrl(k, false, false) - ) - // 临时节点的初始固定坐标为其对应的entity节点坐标,展示直线动画 - // tempNode.fx = node.x - // tempNode.fy = node.y - const tempNodePoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x, y: node.y }, this.getTempNodeAngle(keyCount, i))// 临时节点固定角度,为以entity节点为center,list节点为from,旋转到临时节点的角度 - const finalTempNodePoint = this.pointOfLine({ x: node.x, y: node.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength - if (tempNodePoint.x && tempNodePoint.y) { - tempNode.fx = (node.x + finalTempNodePoint.x) / 2 - tempNode.fy = (node.y + finalTempNodePoint.y) / 2 - } + }) + let nodeIndex = 0// 临时节点(需要total大于0的,所以不可以用循环中的i)角度计算index + Object.keys(node.data.relatedEntities).forEach((k) => { + if (node.data.relatedEntities[k].total) { + // 若已有同级同类型的listNode,不生成此tempNode + const neighbors = node.getNeighbors(this.graph.graphData()) + const hasListNode = neighbors.nodes.some(b => b.data.entityType === k) + if (!hasListNode) { + const tempNode = new Node( + nodeType.tempNode, + `${node.realId}__${k}__temp`, + { + entityType: k, + ...this.generateTempNodeCoordinate(node.getSourceNode(this.graph.graphData()), e) + }, + node, + this.getIconUrl(k, false, false) + ) + // 临时节点的初始固定坐标为其对应的entity节点坐标,展示直线动画 + const tempNodePoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x, y: node.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度,为以entity节点为center,list节点为from,旋转到临时节点的角度 + const finalTempNodePoint = this.pointOfLine({ x: node.x, y: node.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength - const tempLink = new Link(node, tempNode, linkType.temp) - toAddNodes.push(tempNode) - toAddLinks.push(tempLink) + if (tempNodePoint.x && tempNodePoint.y) { + const tempNodeDistance = this.getShortenedLength(tempNode) + this.defaultArrowSize// 临时节点展示动画时临时节点的初始位置距离entity节点的距离 + // 计算箭头角度 + const dx = finalTempNodePoint.x - node.x + const dy = finalTempNodePoint.y - node.y + const angle = Math.atan2(dy, dx) // 计算与x轴的角度(弧度) + tempNode.fx = node.x + tempNodeDistance * Math.cos(angle) + tempNode.fy = node.y + tempNodeDistance * Math.sin(angle) + tempNode.realEndX = finalTempNodePoint.x + tempNode.realEndY = finalTempNodePoint.y } + const tempLink = new Link(node, tempNode, linkType.temp) + toAddNodes.push(tempNode) + toAddLinks.push(tempLink) } - }) - if (toAddNodes.length || toAddLinks.length) { - this.addItems(toAddNodes, toAddLinks, false) } - this.rightBox.node = node - this.rightBox.mode = 'detail' + }) + if (toAddNodes.length || toAddLinks.length) { + this.addItems(toAddNodes, toAddLinks, false) + } + this.rightBox.node = node + this.rightBox.mode = 'detail' + } catch (e) { + console.error(e) + this.$message.error(this.errorMsgHandler(e)) + } finally { + this.rightBox.loading = false + } + } else if (node.type === nodeType.listNode) { + this.rightBox.node = node + this.rightBox.mode = 'list' + } else if (node.type === nodeType.rootNode) { + this.rightBox.node = node + this.rightBox.mode = 'detail' + } + } else { + // 点击tempNode,根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。 + // 若已达第六层,则只生成listNode,不再展开entityNode + const nodes = [] + const links = [] + const stackNodes = [] + const stackLinks = [] + const sourceNode = node.getSourceNode(this.graph.graphData()) + const k1 = (node.x - sourceNode.x) / linkDistance.normal + const k2 = (node.y - sourceNode.y) / linkDistance.normal + const listNode = new Node( + nodeType.listNode, + `${sourceNode.realId}__${node.data.entityType}-list`, + { + entityType: node.data.entityType, + fx: sourceNode.x + k1 * linkDistance.entityToList, + fy: sourceNode.y + k2 * linkDistance.entityToList + }, + sourceNode, + this.getIconUrl(node.data.entityType, false, false) + ) + nodes.push(listNode) + links.push(new Link(sourceNode, listNode)) + stackNodes.push(_.cloneDeep(listNode)) + stackLinks.push(_.cloneDeep(links[0])) + this.addItems(nodes, links, false) + this.cleanTempItems() + this.clickNode = listNode + this.rightBox.mode = 'list' + + setTimeout(async () => { + // 判断listNode的sourceNode层级,若大于等于10(即第6层listNode),则不继续拓展entity node,并给用户提示。否则拓展entity node + const level = this.getNodeLevel(listNode.sourceNode.id) + if (level < 10) { + this.rightBox.loading = true + nodes.splice(0, nodes.length) + links.splice(0, links.length) + try { + const entities = await sourceNode.queryRelatedEntities(listNode.data.entityType) + sourceNode.data.relatedEntities[listNode.data.entityType].list.push(...entities.list) + const curNodes = this.graph.graphData().nodes + entities.list.forEach(entity => { + const entityNode = new Node(nodeType.entityNode, entity.vertex, { + entityType: listNode.data.entityType, + entityName: entity.vertex, + x: listNode.x + Math.random() * 10 - 5, + y: listNode.y + Math.random() * 10 - 5 + }, listNode, this.getIconUrl(listNode.data.entityType, false, false)) + nodes.push(entityNode) + const link = new Link(listNode, entityNode) + links.push(link) + if (curNodes.findIndex(node => node.id === entityNode.id) === -1) { // 过滤掉已有的节点 + stackNodes.push(_.cloneDeep(entityNode)) + } + stackLinks.push(_.cloneDeep(link)) + }) + this.addItems(nodes, links, false) + // 由于点击临时节点后增加节点分为了2步,所以需要将2步的所有节点一起缓存 + this.stackData.undo.push({ nodes: stackNodes, links: stackLinks }) + + setTimeout(() => { + listNode.fx = null + listNode.fy = null + this.rightBox.node = _.cloneDeep(listNode) + }, 100) } catch (e) { console.error(e) this.$message.error(this.errorMsgHandler(e)) } finally { this.rightBox.loading = false } - } else if (node.type === nodeType.listNode) { - this.rightBox.node = node - this.rightBox.mode = 'list' - } else if (node.type === nodeType.rootNode) { - this.rightBox.node = node - this.rightBox.mode = 'detail' + } else { + this.$message.warning(this.$t('tip.maxExpandDepth')) } - } else { - // 点击tempNode,根据source生成listNode和entityNode以及对应的edge。查完entityNode的接口再删除临时node和edge。 - // 若已达第六层,则只生成listNode,不再展开entityNode - const nodes = [] - const links = [] - const stackNodes = [] - const stackLinks = [] - const sourceNode = node.getSourceNode(this.graph.graphData()) - const k1 = (node.x - sourceNode.x) / linkDistance.normal - const k2 = (node.y - sourceNode.y) / linkDistance.normal - const listNode = new Node( - nodeType.listNode, - `${sourceNode.realId}__${node.data.entityType}-list`, - { - entityType: node.data.entityType, - fx: sourceNode.x + k1 * linkDistance.entityToList, - fy: sourceNode.y + k2 * linkDistance.entityToList - }, - sourceNode, - this.getIconUrl(node.data.entityType, false, false) - ) - nodes.push(listNode) - links.push(new Link(sourceNode, listNode)) - stackNodes.push(_.cloneDeep(listNode)) - stackLinks.push(_.cloneDeep(links[0])) - this.addItems(nodes, links, false) - this.cleanTempItems() - this.clickNode = listNode - this.rightBox.mode = 'list' + if (this.stackData.justUndo) { + this.stackData.justUndo = false + this.stackData.redo = [] + } + if (this.stackData.justRedo) { + this.stackData.justRedo = false + this.stackData.redo = [] + } + }, 200) + } + }) + .d3Force('link', d3.forceLink().id(link => link.id) + .distance(link => { + if (link.source.type === nodeType.rootNode) { + return linkDistance.root + } else if (link.source.type === nodeType.entityNode && link.target.type === nodeType.listNode) { + return linkDistance.entityToList + } + return linkDistance.normal + }) + .strength(link => { + return link.strength + }))// 设置线对点的力? + .d3Force('center', d3.forceCenter().strength(0))// 设置力导图点阵中心位置, 向心力设置为0以后,d3.forceCenter(-50,-70)不起作用了 + .d3Force('charge', d3.forceManyBody().strength(this.defaultChargeStrength)) + .onNodeDrag((node, translate) => { + const { nodes, links } = this.graph.graphData() + // 拖动entity节点时,如果此entity节点有临时节点,则同时移动临时节点 + if (node.type === nodeType.entityNode) { + this.isDraggingEntityNode = true + // 查询点击entity节点对应的list节点 + const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x + const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y + const from = { x: fromX, y: fromY } + const centerX = node.preDragX ? node.preDragX : node.x + const centerY = node.preDragY ? node.preDragY : node.y + const center = { x: centerX, y: centerY } - setTimeout(async () => { - // 判断listNode的sourceNode层级,若大于等于10(即第6层listNode),则不继续拓展entity node,并给用户提示。否则拓展entity node - const level = this.getNodeLevel(listNode.sourceNode.id) - if (level < 10) { - this.rightBox.loading = true - nodes.splice(0, nodes.length) - links.splice(0, links.length) - try { - const entities = await sourceNode.queryRelatedEntities(listNode.data.entityType) - sourceNode.data.relatedEntities[listNode.data.entityType].list.push(...entities.list) - const curNodes = this.graph.graphData().nodes - entities.list.forEach(entity => { - const entityNode = new Node(nodeType.entityNode, entity.vertex, { - entityType: listNode.data.entityType, - entityName: entity.vertex, - x: listNode.x + Math.random() * 10 - 5, - y: listNode.y + Math.random() * 10 - 5 - }, listNode, this.getIconUrl(listNode.data.entityType, false, false)) - nodes.push(entityNode) - const link = new Link(listNode, entityNode) - links.push(link) - if (curNodes.findIndex(node => node.id === entityNode.id) === -1) { // 过滤掉已有的节点 - stackNodes.push(_.cloneDeep(entityNode)) - } - stackLinks.push(_.cloneDeep(link)) + const tempLinks = links.filter(link => link.source.id === node.id && link.type === linkType.temp) + tempLinks.forEach(link => { + const tempNodeGroup = nodes.filter(node => node.id === link.target.id) + tempNodeGroup.forEach(tempNode => { + const toX = tempNode.fx ? tempNode.fx : tempNode.x + const toY = tempNode.fy ? tempNode.fy : tempNode.y + const to = { x: toX, y: toY } + if (!tempNode.angle) { // 每次拖拽,每个临时节点计算一次角度即可,因为角度不会发生改变 + tempNode.angle = this.angleOfRotate(from, to, center) + } + + const toPoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x + translate.x, y: node.y + translate.y }, tempNode.angle) + // 因为在拖拽的过长中,list节点到entity节点之间的距离是变化的,且我们要求的是,临时节点到entity节点之间的距离不发生变化,所以此处需要再次进行计算转换,得到最终的坐标 + // 已知2个点的坐标,求从起点开始指定长度线段的终点坐标 + if (!tempNode.lineLength) { // 每次拖拽,每个临时节点计算一次与实体节点的距离即可,因为距离在当前拖拽中不会发生改变 + tempNode.lineLength = Math.sqrt(Math.pow(node.x - tempNode.fx, 2) + Math.pow(node.y - tempNode.fy, 2)) + } + const finalTo = this.pointOfLine({ x: node.x + translate.x, y: node.y + translate.y }, toPoint, tempNode.lineLength) + tempNode.fx = finalTo.x + tempNode.fy = finalTo.y + }) + }) + } + + // 拖动list节点时,如果此list节点对应的entity节点有临时节点,则同时移动临时节点 + if (node.type === nodeType.listNode) { + // 根据list节点,找到list节点连接的线 + const listLinks = links.filter(link => link.source.id === node.id) + const fromX = node.preDragX ? node.preDragX : node.x + const fromY = node.preDragY ? node.preDragY : node.y + listLinks.forEach(link => { + // 找到连接临时节点的虚线 + const tempLinks = links.filter(entityLink => entityLink.source.id === link.target.id && entityLink.type === linkType.temp) + if (tempLinks && tempLinks.length > 0) { + tempLinks.forEach(tempLink => { + // 找到entity节点 + const entityNode = nodes.find(normalNode => normalNode.id === tempLink.source.id && normalNode.type === nodeType.entityNode) + if (entityNode) { + const entityNodeX = entityNode.fx ? entityNode.fx : entityNode.x + const entityNodeY = entityNode.fy ? entityNode.fy : entityNode.y + const angle = this.angleOfRotate({ x: fromX, y: fromY }, { x: node.x + translate.x, y: node.y + translate.y }, { x: entityNodeX, y: entityNodeY }) + const tempNodes = nodes.filter(normalNode => normalNode.id === tempLink.target.id && normalNode.type === nodeType.tempNode)// 找到临时节点 + tempNodes.forEach(tempNode => { + const tempNodeX = tempNode.fx ? tempNode.fx : tempNode.x + const tempNodeY = tempNode.fy ? tempNode.fy : tempNode.y + const sourceNodeX = tempLink.source.fx ? tempLink.source.fx : tempLink.source.x + const sourceNodeY = tempLink.source.fy ? tempLink.source.fy : tempLink.source.y + // rotate为list节点旋转的角度,根据旋转角度,及临时节点的位置,及entity节点的坐标,得到临时节点旋转后的坐标 + const to = this.pointOfRotate({ x: tempNodeX, y: tempNodeY }, { x: sourceNodeX, y: sourceNodeY }, angle) + tempNode.fx = to.x + tempNode.fy = to.y }) - this.addItems(nodes, links, false) - // 由于点击临时节点后增加节点分为了2步,所以需要将2步的所有节点一起缓存 - this.stackData.undo.push({ nodes: stackNodes, links: stackLinks }) - - setTimeout(() => { - listNode.fx = null - listNode.fy = null - this.rightBox.node = _.cloneDeep(listNode) - }, 100) - } catch (e) { - console.error(e) - this.$message.error(this.errorMsgHandler(e)) - } finally { - this.rightBox.loading = false } - } else { - this.$message.warning(this.$t('tip.maxExpandDepth')) - } - if (this.stackData.justUndo) { - this.stackData.justUndo = false - this.stackData.redo = [] - } - if (this.stackData.justRedo) { - this.stackData.justRedo = false - this.stackData.redo = [] - } - }, 200) - } - }) - - .d3Force('link', d3.forceLink().id(link => link.id) - .distance(link => { - if (link.source.type === nodeType.rootNode) { - return linkDistance.root - } else if (link.source.type === nodeType.entityNode && link.target.type === nodeType.listNode) { - return linkDistance.entityToList - } - return linkDistance.normal - }) - .strength(link => { - return link.strength - }))// 设置线对点的力? - .d3Force('center', d3.forceCenter().strength(0))// 设置力导图点阵中心位置, 向心力设置为0以后,d3.forceCenter(-50,-70)不起作用了 - .d3Force('charge', d3.forceManyBody().strength(this.defaultChargeStrength)) - .onNodeDrag((node, translate) => { - const { nodes, links } = this.graph.graphData() - // 拖动entity节点时,如果此entity节点有临时节点,则同时移动临时节点 - if (node.type === nodeType.entityNode) { - // 查询点击entity节点对应的list节点 - const fromX = node.sourceNode.preDragX ? node.sourceNode.preDragX : node.sourceNode.x - const fromY = node.sourceNode.preDragY ? node.sourceNode.preDragY : node.sourceNode.y - const from = { x: fromX, y: fromY } - const centerX = node.preDragX ? node.preDragX : node.x - const centerY = node.preDragY ? node.preDragY : node.y - const center = { x: centerX, y: centerY } - - const tempLinks = links.filter(link => link.source.id === node.id && link.type === linkType.temp) - tempLinks.forEach(link => { - const tempNodeGroup = nodes.filter(node => node.id === link.target.id) - tempNodeGroup.forEach(tempNode => { - const toX = tempNode.fx ? tempNode.fx : tempNode.x - const toY = tempNode.fy ? tempNode.fy : tempNode.y - const to = { x: toX, y: toY } - if (!tempNode.angle) { // 每次拖拽,每个临时节点计算一次角度即可,因为角度不会发生改变 - tempNode.angle = this.angleOfRotate(from, to, center) - } - - const toPoint = this.pointOfRotate({ x: node.sourceNode.x, y: node.sourceNode.y }, { x: node.x + translate.x, y: node.y + translate.y }, tempNode.angle) - // 因为在拖拽的过长中,list节点到entity节点之间的距离是变化的,且我们要求的是,临时节点到entity节点之间的距离不发生变化,所以此处需要再次进行计算转换,得到最终的坐标 - // 已知2个点的坐标,求从起点开始指定长度线段的终点坐标 - if (!tempNode.lineLength) { // 每次拖拽,每个临时节点计算一次与实体节点的距离即可,因为距离在当前拖拽中不会发生改变 - tempNode.lineLength = Math.sqrt(Math.pow(node.x - tempNode.fx, 2) + Math.pow(node.y - tempNode.fy, 2)) - } - const finalTo = this.pointOfLine({ x: node.x + translate.x, y: node.y + translate.y }, toPoint, tempNode.lineLength) - tempNode.fx = finalTo.x - tempNode.fy = finalTo.y }) - }) - } - - // 拖动list节点时,如果此list节点对应的entity节点有临时节点,则同时移动临时节点 - if (node.type === nodeType.listNode) { - // 根据list节点,找到list节点连接的线 - const listLinks = links.filter(link => link.source.id === node.id) - const fromX = node.preDragX ? node.preDragX : node.x - const fromY = node.preDragY ? node.preDragY : node.y - listLinks.forEach(link => { - // 找到连接临时节点的虚线 - const tempLinks = links.filter(entityLink => entityLink.source.id === link.target.id && entityLink.type === linkType.temp) - if (tempLinks && tempLinks.length > 0) { - tempLinks.forEach(tempLink => { - // 找到entity节点 - const entityNode = nodes.find(normalNode => normalNode.id === tempLink.source.id && normalNode.type === nodeType.entityNode) - if (entityNode) { - const entityNodeX = entityNode.fx ? entityNode.fx : entityNode.x - const entityNodeY = entityNode.fy ? entityNode.fy : entityNode.y - const angle = this.angleOfRotate({ x: fromX, y: fromY }, { x: node.x + translate.x, y: node.y + translate.y }, { x: entityNodeX, y: entityNodeY }) - const tempNodes = nodes.filter(normalNode => normalNode.id === tempLink.target.id && normalNode.type === nodeType.tempNode)// 找到临时节点 - tempNodes.forEach(tempNode => { - const tempNodeX = tempNode.fx ? tempNode.fx : tempNode.x - const tempNodeY = tempNode.fy ? tempNode.fy : tempNode.y - const sourceNodeX = tempLink.source.fx ? tempLink.source.fx : tempLink.source.x - const sourceNodeY = tempLink.source.fy ? tempLink.source.fy : tempLink.source.y - // rotate为list节点旋转的角度,根据旋转角度,及临时节点的位置,及entity节点的坐标,得到临时节点旋转后的坐标 - const to = this.pointOfRotate({ x: tempNodeX, y: tempNodeY }, { x: sourceNodeX, y: sourceNodeY }, angle) - tempNode.fx = to.x - tempNode.fy = to.y - }) - } - }) - } - }) - } - // 记录上次拖拽时节点的坐标,否则计算时出现误差。 - node.preDragX = node.x + translate.x - node.preDragY = node.y + translate.y - }) - .cooldownTime(2000)// 到时间后,才执行onEngineStop - .onNodeDragEnd((node, translate) => { // 修复拖动节点 - node.fx = node.x - node.fy = node.y - // 拖拽结束时,把所有临时节点的的angle置为null,便于拖动其它节点时的计算 - this.graph.graphData().nodes.forEach(node => { - if (node.type === nodeType.tempNode) { - node.angle = null - node.lineLength = null } }) - }) - .onEngineTick(() => { - if (this.isClicking) { - const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode) - if (tempNodeGroup.length > 0) { - const keyCount = tempNodeGroup.length - tempNodeGroup.forEach((tempNode, i) => { - const tempNodeSource = tempNode.sourceNode - if (tempNodeSource) { - const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, i))// 临时节点固定角度,为以entity节点为center,list节点为from,旋转到临时节点的角度 - const finalTempNodePoint = this.pointOfLine({ x: tempNodeSource.x, y: tempNodeSource.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength - if (tempNodePoint.x && tempNodePoint.y) { - tempNode.fx = finalTempNodePoint.x - tempNode.fy = finalTempNodePoint.y - } - } - }) - this.isClicking = false - } + } + // 记录上次拖拽时节点的坐标,否则计算时出现误差。 + node.preDragX = node.x + translate.x + node.preDragY = node.y + translate.y + }) + .cooldownTime(2000)// 到时间后,才执行onEngineStop + .onNodeDragEnd((node, translate) => { // 修复拖动节点 + this.isDraggingEntityNode = false + node.fx = node.x + node.fy = node.y + // 拖拽结束时,把所有临时节点的的angle置为null,便于拖动其它节点时的计算 + this.graph.graphData().nodes.forEach(node => { + if (node.type === nodeType.tempNode) { + node.angle = null + node.lineLength = null } }) + }) + .onEngineTick(() => { + if (this.isClicking) { + const tempNodeGroup = this.graph.graphData().nodes.filter(node => node.type === nodeType.tempNode) + if (tempNodeGroup.length > 0) { + const keyCount = tempNodeGroup.length + let nodeIndex = 0// 临时节点(需要total大于0的,所以不可以用循环中的i)角度计算index + tempNodeGroup.forEach((tempNode) => { + const tempNodeSource = tempNode.sourceNode + if (tempNodeSource) { + const tempNodePoint = this.pointOfRotate({ x: tempNodeSource.sourceNode.x, y: tempNodeSource.sourceNode.y }, { x: tempNodeSource.x, y: tempNodeSource.y }, this.getTempNodeAngle(keyCount, nodeIndex++))// 临时节点固定角度,为以entity节点为center,list节点为from,旋转到临时节点的角度 + // const finalTempNodePoint = this.pointOfLine({ x: tempNodeSource.x, y: tempNodeSource.y }, tempNodePoint, this.defaultLinkDistance)// start,end,lineLength + if (tempNodePoint.x && tempNodePoint.y) { + // tempNode.fx = finalTempNodePoint.x + // tempNode.fy = finalTempNodePoint.y + tempNode.fx = tempNode.realEndX + tempNode.fy = tempNode.realEndY + } + } + }) + this.isClicking = false + } + } + }) + }, + /* + * 线性插值: 已知两点坐标,计算已知x或y的C点坐标,且C点在AB连线上 + * + * */ + linearInterpolate (coord1, coord2) { + const scale = (coord1[0] - coord2[0]) / (coord1[1] - coord2[1]) + return { + getY: (x) => (x - coord1[0]) / scale + coord1[1], + getX: (y) => (y - coord1[1]) / scale + coord1[0] + } }, getTempNodeAngle (nodeCount, i) { switch (nodeCount) { @@ -850,10 +944,10 @@ export default { } }, /** - * 求直线与圆的交点坐标 - * 圆:x2, y2, r - * 线段: x1, y1, x2, y2 - * */ + * 求直线与圆的交点坐标 + * 圆:x2, y2, r + * 线段: x1, y1, x2, y2 + * */ findCircleLineIntersection (x2, y2, r, x1, y1) { r = r + +this.defaultMargin const xx = x1 - x2 @@ -879,14 +973,14 @@ export default { return [{ x, y }] }, /** - ctx :Canvas绘图环境 - fromX, fromY :起点坐标(也可以换成 p1 ,只不过它是一个数组) - toX, toY :终点坐标 (也可以换成 p2 ,只不过它是一个数组) - theta :三角斜边一直线夹角 - headlen :三角斜边长度 - width :箭头线宽度 - color :箭头颜色 - */ + ctx :Canvas绘图环境 + fromX, fromY :起点坐标(也可以换成 p1 ,只不过它是一个数组) + toX, toY :终点坐标 (也可以换成 p2 ,只不过它是一个数组) + theta :三角斜边一直线夹角 + headlen :三角斜边长度 + width :箭头线宽度 + color :箭头颜色 + */ drawArrow (ctx, fromX, fromY, toX, toY, theta, headlen) { theta = typeof (theta) != 'undefined' ? theta : 30 headlen = typeof (theta) != 'undefined' ? headlen : 10 @@ -989,12 +1083,12 @@ export default { if (rootNode.data.relatedEntities[k].total) { const listNode = new Node( nodeType.listNode, - `${rootNode.realId}__${k}-list`, - { - entityType: k - }, - rootNode, - this.getIconUrl(k, false, false) + `${rootNode.realId}__${k}-list`, + { + entityType: k + }, + rootNode, + this.getIconUrl(k, false, false) ) listNodes.push(listNode) links.push(new Link(rootNode, listNode))