/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { select, event } from "d3-selection"; import { zoom, zoomIdentity } from "d3-zoom"; import { drag } from "d3-drag"; import { line, curveBasis } from "d3-shape"; import platform from "platform"; import Enums from "../Enums"; const LineageUtils = { /** * [nodeArrowDistance variable use to define the distance between arrow and node] * @type {Number} */ nodeArrowDistance: 24, refreshGraphForSafari: function (options) { var edgePathEl = options.edgeEl, IEGraphRenderDone = 0; edgePathEl.each(function (argument) { var eleRef = this, childNode = $(this).find("pattern"); setTimeout(function (argument) { $(eleRef).find("defs").append(childNode); }, 500); }); }, refreshGraphForIE: function ({ edgePathEl }) { var IEGraphRenderDone = 0; edgePathEl.each(function (argument) { var childNode = $(this).find("marker"); $(this).find("marker").remove(); var eleRef = this; ++IEGraphRenderDone; setTimeout(function (argument) { $(eleRef).find("defs").append(childNode); --IEGraphRenderDone; if (IEGraphRenderDone === 0) { this.$(".fontLoader").hide(); this.$("svg").fadeTo(1000, 1); } }, 1000); }); }, /** * [dragNode description] * @param {[type]} options.g [description] * @param {[type]} options.svg [description] * @param {[type]} options.guid [description] * @param {[type]} options.edgePathEl [description] * @return {[type]} [description] */ dragNode: function ({ g, svg, guid, edgePathEl }) { var dragHelper = { dragmove: function (el, d) { var node = select(el), selectedNode = g.node(d), prevX = selectedNode.x, prevY = selectedNode.y; selectedNode.x += event.dx; selectedNode.y += event.dy; node.attr("transform", "translate(" + selectedNode.x + "," + selectedNode.y + ")"); var dx = selectedNode.x - prevX, dy = selectedNode.y - prevY; g.edges().forEach((e) => { if (e.v == d || e.w == d) { var edge = g.edge(e.v, e.w); this.translateEdge(edge, dx, dy); select(edge.elem).select("path").attr("d", this.calcPoints(e)); } }); //LineageUtils.refreshGraphForIE({ edgePathEl: edgePathEl }); }, translateEdge: function (e, dx, dy) { e.points.forEach(function (p) { p.x = p.x + dx; p.y = p.y + dy; }); }, calcPoints: function (e) { var edge = g.edge(e.v, e.w), tail = g.node(e.v), head = g.node(e.w), points = edge.points.slice(1, edge.points.length - 1), afterslice = edge.points.slice(1, edge.points.length - 1); points.unshift(this.intersectRect(tail, points[0])); points.push(this.intersectRect(head, points[points.length - 1])); return line() .x(function (d) { return d.x; }) .y(function (d) { return d.y; }) .curve(curveBasis)(points); }, intersectRect: (node, point) => { var x = node.x, y = node.y, dx = point.x - x, dy = point.y - y, nodeDistance = guid ? this.nodeArrowDistance + 3 : this.nodeArrowDistance, w = nodeDistance, h = nodeDistance, sx = 0, sy = 0; if (Math.abs(dy) * w > Math.abs(dx) * h) { // Intersection is top or bottom of rect. if (dy < 0) { h = -h; } sx = dy === 0 ? 0 : (h * dx) / dy; sy = h; } else { // Intersection is left or right of rect. if (dx < 0) { w = -w; } sx = w; sy = dx === 0 ? 0 : (w * dy) / dx; } return { x: x + sx, y: y + sy }; } }; var dragNodeHandler = drag().on("drag", function (d) { dragHelper.dragmove.call(dragHelper, this, d); }), dragEdgePathHandler = drag().on("drag", function (d) { dragHelper.translateEdge(g.edge(d.v, d.w), event.dx, event.dy); var edgeObj = g.edge(d.v, d.w); select(edgeObj.elem).select("path").attr("d", dragHelper.calcPoints(d)); }); dragNodeHandler(svg.selectAll("g.node")); dragEdgePathHandler(svg.selectAll("g.edgePath")); }, zoomIn: function ({ svg, scaleFactor = 1.3 }) { this.d3Zoom.scaleBy(svg.transition().duration(750), scaleFactor); }, zoomOut: function ({ svg, scaleFactor = 0.8 }) { this.d3Zoom.scaleBy(svg.transition().duration(750), scaleFactor); }, zoom: function ({ svg, xa, ya, scale }) { svg.transition().duration(750).call(this.d3Zoom.transform, zoomIdentity.translate(xa, ya).scale(scale)); }, fitToScreen: function ({ svg }) { var node = svg.node(); var bounds = node.getBBox(); var parent = node.parentElement, fullWidth = parent.clientWidth, fullHeight = parent.clientHeight; var width = bounds.width, height = bounds.height; var midX = bounds.x + width / 2, midY = bounds.y + height / 2; var scale = (scale || 0.95) / Math.max(width / fullWidth, height / fullHeight), xa = fullWidth / 2 - scale * midX, ya = fullHeight / 2 - scale * midY; this.zoom({ svg, xa, ya, scale }); }, /** * [centerNode description] * @param {[type]} options.guid [description] * @param {[type]} options.g [description] * @param {[type]} options.svg [description] * @param {[type]} options.svgGroupEl [description] * @param {[type]} options.edgePathEl [description] * @param {[type]} options.width [description] * @param {[type]} options.height [description] * @param {[type]} options.onCenterZoomed [description] * @return {[type]} [description] */ centerNode: function ({ guid, g, svg, svgGroupEl, edgePathEl, width, height, fitToScreen, onCenterZoomed }) { this.d3Zoom = zoom(); svg.call(this.d3Zoom).on("dblclick.zoom", null); // restrict events let selectedNodeEl = svg.selectAll("g.nodes>g[id='" + guid + "']"), zoomListener = this.d3Zoom.scaleExtent([0.01, 50]).on("zoom", function () { svgGroupEl.attr("transform", event.transform); }), x = null, y = null, scale = 1.2; if (selectedNodeEl.empty()) { if (fitToScreen) { this.fitToScreen({ svg }); return; } else { x = g.graph().width / 2; y = g.graph().height / 2; } } else { var matrix = selectedNodeEl .attr("transform") .replace(/[^0-9\-.,]/g, "") .split(","); // if (platform.name === "IE" || platform.name === "Microsoft Edge") { // var matrix = selectedNode // .attr("transform") // .replace(/[a-z\()]/g, "") // .split(" "); // } x = matrix[0]; y = matrix[1]; } var xa = -(x * scale - width / 2), ya = -(y * scale - height / 2); this.zoom({ svg, xa, ya, scale }); svg.transition().duration(750).call(this.d3Zoom.transform, zoomIdentity.translate(xa, ya).scale(scale)); if (onCenterZoomed) { onCenterZoomed({ newScale: scale, newTranslate: [xa, ya], d3Zoom: this.d3Zoom, selectedNodeEl }); } // if (platform.name === "IE") { // LineageUtils.refreshGraphForIE({ edgePathEl: edgePathEl }); // } }, /** * [getToolTipDirection description] * @param {[type]} options.el [description] * @return {[type]} [description] */ getToolTipDirection: function ({ el }) { var width = select("body").node().getBoundingClientRect().width, currentELWidth = select(el).node().getBoundingClientRect(), direction = "e"; if (width - currentELWidth.left < 330) { direction = width - currentELWidth.left < 330 && currentELWidth.top < 400 ? "sw" : "w"; if (width - currentELWidth.left < 330 && currentELWidth.top > 600) { direction = "nw"; } } else if (currentELWidth.top > 600) { direction = width - currentELWidth.left < 330 && currentELWidth.top > 600 ? "nw" : "n"; if (currentELWidth.left < 50) { direction = "ne"; } } else if (currentELWidth.top < 400) { direction = currentELWidth.left < 50 ? "se" : "s"; } return direction; }, /** * [onHoverFade description] * @param {[type]} options.svg [description] * @param {[type]} options.g [description] * @param {[type]} options.mouseenter [description] * @param {[type]} options.opacity [description] * @param {[type]} options.nodesToHighlight [description] * @param {[type]} options.hoveredNode [description] * @return {[type]} [description] */ onHoverFade: function ({ svg, g, mouseenter, nodesToHighlight, hoveredNode }) { var node = svg.selectAll(".node"), path = svg.selectAll(".edgePath"), isConnected = function (a, b, o) { if (a === o || (b && b.length && b.indexOf(o) != -1)) { return true; } }; if (mouseenter) { svg.classed("hover", true); var nextNode = g.successors(hoveredNode), previousNode = g.predecessors(hoveredNode), nodesToHighlight = nextNode.concat(previousNode); node.classed("hover-active-node", function (currentNode, i, nodes) { if (isConnected(hoveredNode, nodesToHighlight, currentNode)) { return true; } else { return false; } }); path.classed("hover-active-path", function (c) { var _thisOpacity = c.v === hoveredNode || c.w === hoveredNode ? 1 : 0; if (_thisOpacity) { return true; } else { return false; } }); } else { svg.classed("hover", false); node.classed("hover-active-node", false); path.classed("hover-active-path", false); } }, /** * [getBaseUrl description] * @param {[type]} path [description] * @return {[type]} [description] */ getBaseUrl: function (url = window.location.pathname) { return url.replace(/\/[\w-]+.(jsp|html)|\/+$/gi, ""); }, getEntityIconPath: function ({ entityData, errorUrl, imgBasePath }) { var iconBasePath = this.getBaseUrl() + (imgBasePath || "/img/entity-icon/"); if (entityData) { let { typeName, serviceType, status, isProcess } = entityData; function getImgPath(imageName) { return iconBasePath + (Enums.entityStateReadOnly[status] ? "disabled/" + imageName : imageName); } function getDefaultImgPath() { if (isProcess) { if (Enums.entityStateReadOnly[status]) { return iconBasePath + "disabled/process.png"; } else { return iconBasePath + "process.png"; } } else { if (Enums.entityStateReadOnly[status]) { return iconBasePath + "disabled/table.png"; } else { return iconBasePath + "table.png"; } } } if (errorUrl) { // Check if the default img path has error, if yes then stop recursion. if (errorUrl.indexOf("table.png") > -1 || errorUrl.indexOf("process.png") > -1) { return null; } var isErrorInTypeName = errorUrl && errorUrl.match("entity-icon/" + typeName + ".png|disabled/" + typeName + ".png") ? true : false; if (serviceType && isErrorInTypeName) { var imageName = serviceType + ".png"; return getImgPath(imageName); } else { return getDefaultImgPath(); } } else if (typeName) { var imageName = typeName + ".png"; return getImgPath(imageName); } else if (serviceType) { var imageName = serviceType + ".png"; return getImgPath(imageName); } else { return getDefaultImgPath(); } } }, base64Encode: function (file, callback) { const reader = new FileReader(); reader.addEventListener("load", () => callback(reader.result)); reader.readAsDataURL(file); }, imgShapeRender: function (parent, bbox, node, { dagreD3, defsEl, imgBasePath, guid, isRankdirToBottom }) { var that = this, viewGuid = guid, imageIconPath = this.getEntityIconPath({ entityData: node, imgBasePath }), imgName = imageIconPath.split("/").pop(); if (this.imageObject === undefined) { this.imageObject = {}; } if (node.isDeleted) { imgName = "deleted_" + imgName; } if (node.id == viewGuid) { var currentNode = true; } var shapeSvg = parent .append("circle") .attr("fill", "url(#img_" + imgName + ")") .attr("r", isRankdirToBottom ? "30px" : "24px") .attr("data-stroke", node.id) .attr("stroke-width", "2px") .attr("class", "nodeImage " + (currentNode ? "currentNode" : node.isProcess ? "process" : "node")); if (currentNode) { shapeSvg.attr("stroke", "#fb4200"); } if (node.isIncomplete === true) { parent.attr("class", "node isIncomplete show"); parent .insert("foreignObject") .attr("x", "-25") .attr("y", "-25") .attr("width", "50") .attr("height", "50") .append("xhtml:div") .insert("i") .attr("class", "fa fa-hourglass-half"); } if (defsEl.select('pattern[id="img_' + imgName + '"]').empty()) { defsEl .append("pattern") .attr("x", "0%") .attr("y", "0%") .attr("patternUnits", "objectBoundingBox") .attr("id", "img_" + imgName) .attr("width", "100%") .attr("height", "100%") .append("image") .attr("href", function (d) { var imgEl = this; if (node) { var getImageData = function (options) { var imagePath = options.imagePath, ajaxOptions = { url: imagePath, method: "GET", cache: true }; // if (platform.name !== "IE") { // ajaxOptions["mimeType"] = "text/plain; charset=x-user-defined"; // } shapeSvg.attr("data-iconpath", imagePath); var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status === 200) { if (platform.name !== "IE") { that.base64Encode(this.response, (url) => { that.imageObject[imageIconPath] = url; select(imgEl).attr("xlink:href", url); }); } else { that.imageObject[imageIconPath] = imagePath; } if (imageIconPath !== shapeSvg.attr("data-iconpath")) { shapeSvg.attr("data-iconpathorigin", imageIconPath); } } else if (xhr.status === 404) { const imgPath = that.getEntityIconPath({ entityData: node, errorUrl: imagePath }); if (imgPath === null) { const patternEL = select(imgEl.parentElement); patternEL.select("image").remove(); patternEL .attr("patternContentUnits", "objectBoundingBox") .append("circle") .attr("r", "24px") .attr("fill", "#e8e8e8"); } else { getImageData({ imagePath: imgPath }); } } } }; xhr.responseType = "blob"; xhr.open(ajaxOptions.method, ajaxOptions.url, true); xhr.send(null); }; getImageData({ imagePath: imageIconPath }); } }) .attr("x", isRankdirToBottom ? "11" : "4") .attr("y", isRankdirToBottom ? "20" : currentNode ? "3" : "4") .attr("width", "40") .attr("height", "40"); } node.intersect = function (point) { return dagreD3.intersect.circle(node, currentNode ? that.nodeArrowDistance + 3 : that.nodeArrowDistance, point); }; return shapeSvg; }, /** * [arrowPointRender description] * @param {[type]} {parent, id, edge, type, viewOptions [description] * @return {[type]} [description] */ arrowPointRender: function (parent, id, edge, type, { dagreD3 }) { var node = parent.node(), parentNode = node ? node.parentNode : parent; select(parentNode) .select("path.path") .attr("marker-end", "url(#" + id + ")"); var marker = parent .append("marker") .attr("id", id) .attr("viewBox", "0 0 10 10") .attr("refX", 8) .attr("refY", 5) .attr("markerUnits", "strokeWidth") .attr("markerWidth", 4) .attr("markerHeight", 4) .attr("orient", "auto"); var path = marker.append("path").attr("d", "M 0 0 L 10 5 L 0 10 z").style("fill", edge.styleObj.stroke); dagreD3.util.applyStyle(path, edge[type + "Style"]); }, /** * [saveSvg description] * @param {[type]} options.svg [description] * @param {[type]} options.width [description] * @param {[type]} options.height [description] * @param {[type]} options.downloadFileName [description] * @param {[type]} options.onExportLineage [description] * @return {[type]} [description] */ saveSvg: function ({ svg, width, height, downloadFileName, onExportLineage }) { var that = this, svgClone = svg.clone(true).node(), scaleFactor = 1; setTimeout(function () { if (platform.name === "Firefox") { svgClone.setAttribute("width", width); svgClone.setAttribute("height", height); } const hiddenSvgEl = select("body").append("div"); hiddenSvgEl.classed("hidden-svg", true); hiddenSvgEl.node().appendChild(svgClone); const svgCloneEl = select(".hidden-svg svg"); svgCloneEl.select("g").attr("transform", "scale(" + scaleFactor + ")"); svgCloneEl.select("foreignObject").remove(); var canvasOffset = { x: 150, y: 150 }, setWidth = svgClone.getBBox().width + canvasOffset.x, setHeight = svgClone.getBBox().height + canvasOffset.y, xAxis = svgClone.getBBox().x, yAxis = svgClone.getBBox().y; svgClone.attributes.viewBox.value = xAxis + "," + yAxis + "," + setWidth + "," + setHeight; var canvas = document.createElement("canvas"); canvas.id = "canvas"; canvas.style.display = "none"; canvas.width = svgClone.getBBox().width * scaleFactor + canvasOffset.x; canvas.height = svgClone.getBBox().height * scaleFactor + canvasOffset.y; // Append Canvas in DOM select("body").node().appendChild(canvas); var ctx = canvas.getContext("2d"), data = new XMLSerializer().serializeToString(svgClone), DOMURL = window.URL || window.webkitURL || window; ctx.fillStyle = "#FFFFFF"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeRect(0, 0, canvas.width, canvas.height); ctx.restore(); var img = new Image(canvas.width, canvas.height); var svgBlob = new Blob([data], { type: "image/svg+xml;base64" }); if (platform.name === "Safari") { svgBlob = new Blob([data], { type: "image/svg+xml" }); } var url = DOMURL.createObjectURL(svgBlob); img.onload = function () { try { var a = document.createElement("a"); a.download = downloadFileName; document.body.appendChild(a); ctx.drawImage(img, 50, 50, canvas.width, canvas.height); canvas.toBlob(function (blob) { if (!blob) { onExportLineage({ status: "failed", message: "There was an error in downloading Lineage!" }); return; } a.href = DOMURL.createObjectURL(blob); if (blob.size > 10000000) { onExportLineage({ status: "failed", message: "The Image size is huge, please open the image in a browser!" }); } a.click(); onExportLineage({ status: "Success", message: "Successful" }); if (platform.name === "Safari") { that.refreshGraphForSafari({ edgeEl: that.$("svg g.node") }); } }, "image/png"); hiddenSvgEl.remove(); canvas.remove(); } catch (err) { onExportLineage({ status: "failed", message: "There was an error in downloading Lineage!" }); } }; img.src = url; }, 0); } }; export default LineageUtils;