n return 0;\n } else {\n return res.graph;\n }\n};\n/**\n * @brief : Auxiliary function used for LCA computation\n *\n * @arg node1 : node1's ID\n * @arg node2 : node2's ID\n * @arg graphIx : subgraph index\n * @arg layoutInfo : layoutInfo object\n *\n * @return : object of the form {count: X, graph: Y}, where:\n * X is the number of ancesters (max: 2) found in\n * graphIx (and it's subgraphs),\n * Y is the graph index of the lowest graph containing\n * all X nodes\n */\n\n\nvar findLCA_aux = function findLCA_aux(node1, node2, graphIx, layoutInfo) {\n var graph = layoutInfo.graphSet[graphIx]; // If both nodes belongs to graphIx\n\n if (-1 < graph.indexOf(node1) && -1 < graph.indexOf(node2)) {\n return {\n count: 2,\n graph: graphIx\n };\n } // Make recursive calls for all subgraphs\n\n\n var c = 0;\n\n for (var i = 0; i < graph.length; i++) {\n var nodeId = graph[i];\n var nodeIx = layoutInfo.idToIndex[nodeId];\n var children = layoutInfo.layoutNodes[nodeIx].children; // If the node has no child, skip it\n\n if (0 === children.length) {\n continue;\n }\n\n var childGraphIx = layoutInfo.indexToGraph[layoutInfo.idToIndex[children[0]]];\n var result = findLCA_aux(node1, node2, childGraphIx, layoutInfo);\n\n if (0 === result.count) {\n // Neither node1 nor node2 are present in this subgraph\n continue;\n } else if (1 === result.count) {\n // One of (node1, node2) is present in this subgraph\n c++;\n\n if (2 === c) {\n // We've already found both nodes, no need to keep searching\n break;\n }\n } else {\n // Both nodes are present in this subgraph\n return result;\n }\n }\n\n return {\n count: c,\n graph: graphIx\n };\n};\n/**\n * @brief: printsLayoutInfo into js console\n * Only used for debbuging\n */\n\n\nif (false) {\n var printLayoutInfo;\n}\n/**\n * @brief : Randomizes the position of all nodes\n */\n\n\nvar randomizePositions = function randomizePositions(layoutInfo, cy) {\n var width = layoutInfo.clientWidth;\n var height = layoutInfo.clientHeight;\n\n for (var i = 0; i < layoutInfo.nodeSize; i++) {\n var n = layoutInfo.layoutNodes[i]; // No need to randomize compound nodes or locked nodes\n\n if (0 === n.children.length && !n.isLocked) {\n n.positionX = Math.random() * width;\n n.positionY = Math.random() * height;\n }\n }\n};\n\nvar getScaleInBoundsFn = function getScaleInBoundsFn(layoutInfo, options, nodes) {\n var bb = layoutInfo.boundingBox;\n var coseBB = {\n x1: Infinity,\n x2: -Infinity,\n y1: Infinity,\n y2: -Infinity\n };\n\n if (options.boundingBox) {\n nodes.forEach(function (node) {\n var lnode = layoutInfo.layoutNodes[layoutInfo.idToIndex[node.data('id')]];\n coseBB.x1 = Math.min(coseBB.x1, lnode.positionX);\n coseBB.x2 = Math.max(coseBB.x2, lnode.positionX);\n coseBB.y1 = Math.min(coseBB.y1, lnode.positionY);\n coseBB.y2 = Math.max(coseBB.y2, lnode.positionY);\n });\n coseBB.w = coseBB.x2 - coseBB.x1;\n coseBB.h = coseBB.y2 - coseBB.y1;\n }\n\n return function (ele, i) {\n var lnode = layoutInfo.layoutNodes[layoutInfo.idToIndex[ele.data('id')]];\n\n if (options.boundingBox) {\n // then add extra bounding box constraint\n var pctX = (lnode.positionX - coseBB.x1) / coseBB.w;\n var pctY = (lnode.positionY - coseBB.y1) / coseBB.h;\n return {\n x: bb.x1 + pctX * bb.w,\n y: bb.y1 + pctY * bb.h\n };\n } else {\n return {\n x: lnode.positionX,\n y: lnode.positionY\n };\n }\n };\n};\n/**\n * @brief : Updates the positions of nodes in the network\n * @arg layoutInfo : LayoutInfo object\n * @arg cy : Cytoscape object\n * @arg options : Layout options\n */\n\n\nvar refreshPositions = function refreshPositions(layoutInfo, cy, options) {\n // var s = 'Refreshing positions';\n // logDebug(s);\n var layout = options.layout;\n var nodes = options.eles.nodes();\n var getScaledPos = getScaleInBoundsFn(layoutInfo, options, nodes);\n nodes.positions(getScaledPos); // Trigger layoutReady only on first call\n\n if (true !== layoutInfo.ready) {\n // s = 'Triggering layoutready';\n // logDebug(s);\n layoutInfo.ready = true;\n layout.one('layoutready', options.ready);\n layout.emit({\n type: 'layoutready',\n layout: this\n });\n }\n};\n/**\n * @brief : Logs a debug message in JS console, if DEBUG is ON\n */\n// var logDebug = function(text) {\n// if (DEBUG) {\n// console.debug(text);\n// }\n// };\n\n/**\n * @brief : Performs one iteration of the physical simulation\n * @arg layoutInfo : LayoutInfo object already initialized\n * @arg cy : Cytoscape object\n * @arg options : Layout options\n */\n\n\nvar step$1 = function step(layoutInfo, options, _step) {\n // var s = \"\\n\\n###############################\";\n // s += \"\\nSTEP: \" + step;\n // s += \"\\n###############################\\n\";\n // logDebug(s);\n // Calculate node repulsions\n calculateNodeForces(layoutInfo, options); // Calculate edge forces\n\n calculateEdgeForces(layoutInfo); // Calculate gravity forces\n\n calculateGravityForces(layoutInfo, options); // Propagate forces from parent to child\n\n propagateForces(layoutInfo); // Update positions based on calculated forces\n\n updatePositions(layoutInfo);\n};\n/**\n * @brief : Computes the node repulsion forces\n */\n\n\nvar calculateNodeForces = function calculateNodeForces(layoutInfo, options) {\n // Go through each of the graphs in graphSet\n // Nodes only repel each other if they belong to the same graph\n // var s = 'calculateNodeForces';\n // logDebug(s);\n for (var i = 0; i < layoutInfo.graphSet.length; i++) {\n var graph = layoutInfo.graphSet[i];\n var numNodes = graph.length; // s = \"Set: \" + graph.toString();\n // logDebug(s);\n // Now get all the pairs of nodes\n // Only get each pair once, (A, B) = (B, A)\n\n for (var j = 0; j < numNodes; j++) {\n var node1 = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[j]]];\n\n for (var k = j + 1; k < numNodes; k++) {\n var node2 = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[k]]];\n nodeRepulsion(node1, node2, layoutInfo, options);\n }\n }\n }\n};\n\nvar randomDistance = function randomDistance(max) {\n return -max + 2 * max * Math.random();\n};\n/**\n * @brief : Compute the node repulsion forces between a pair of nodes\n */\n\n\nvar nodeRepulsion = function nodeRepulsion(node1, node2, layoutInfo, options) {\n // var s = \"Node repulsion. Node1: \" + node1.id + \" Node2: \" + node2.id;\n var cmptId1 = node1.cmptId;\n var cmptId2 = node2.cmptId;\n\n if (cmptId1 !== cmptId2 && !layoutInfo.isCompound) {\n return;\n } // Get direction of line connecting both node centers\n\n\n var directionX = node2.positionX - node1.positionX;\n var directionY = node2.positionY - node1.positionY;\n var maxRandDist = 1; // s += \"\\ndirectionX: \" + directionX + \", directionY: \" + directionY;\n // If both centers are the same, apply a random force\n\n if (0 === directionX && 0 === directionY) {\n directionX = randomDistance(maxRandDist);\n directionY = randomDistance(maxRandDist);\n }\n\n var overlap = nodesOverlap(node1, node2, directionX, directionY);\n\n if (overlap > 0) {\n // s += \"\\nNodes DO overlap.\";\n // s += \"\\nOverlap: \" + overlap;\n // If nodes overlap, repulsion force is proportional\n // to the overlap\n var force = options.nodeOverlap * overlap; // Compute the module and components of the force vector\n\n var distance = Math.sqrt(directionX * directionX + directionY * directionY); // s += \"\\nDistance: \" + distance;\n\n var forceX = force * directionX / distance;\n var forceY = force * directionY / distance;\n } else {\n // s += \"\\nNodes do NOT overlap.\";\n // If there's no overlap, force is inversely proportional\n // to squared distance\n // Get clipping points for both nodes\n var point1 = findClippingPoint(node1, directionX, directionY);\n var point2 = findClippingPoint(node2, -1 * directionX, -1 * directionY); // Use clipping points to compute distance\n\n var distanceX = point2.x - point1.x;\n var distanceY = point2.y - point1.y;\n var distanceSqr = distanceX * distanceX + distanceY * distanceY;\n var distance = Math.sqrt(distanceSqr); // s += \"\\nDistance: \" + distance;\n // Compute the module and components of the force vector\n\n var force = (node1.nodeRepulsion + node2.nodeRepulsion) / distanceSqr;\n var forceX = force * distanceX / distance;\n var forceY = force * distanceY / distance;\n } // Apply force\n\n\n if (!node1.isLocked) {\n node1.offsetX -= forceX;\n node1.offsetY -= forceY;\n }\n\n if (!node2.isLocked) {\n node2.offsetX += forceX;\n node2.offsetY += forceY;\n } // s += \"\\nForceX: \" + forceX + \" ForceY: \" + forceY;\n // logDebug(s);\n\n\n return;\n};\n/**\n * @brief : Determines whether two nodes overlap or not\n * @return : Amount of overlapping (0 => no overlap)\n */\n\n\nvar nodesOverlap = function nodesOverlap(node1, node2, dX, dY) {\n if (dX > 0) {\n var overlapX = node1.maxX - node2.minX;\n } else {\n var overlapX = node2.maxX - node1.minX;\n }\n\n if (dY > 0) {\n var overlapY = node1.maxY - node2.minY;\n } else {\n var overlapY = node2.maxY - node1.minY;\n }\n\n if (overlapX >= 0 && overlapY >= 0) {\n return Math.sqrt(overlapX * overlapX + overlapY * overlapY);\n } else {\n return 0;\n }\n};\n/**\n * @brief : Finds the point in which an edge (direction dX, dY) intersects\n * the rectangular bounding box of it's source/target node\n */\n\n\nvar findClippingPoint = function findClippingPoint(node, dX, dY) {\n // Shorcuts\n var X = node.positionX;\n var Y = node.positionY;\n var H = node.height || 1;\n var W = node.width || 1;\n var dirSlope = dY / dX;\n var nodeSlope = H / W; // var s = 'Computing clipping point of node ' + node.id +\n // \" . Height: \" + H + \", Width: \" + W +\n // \"\\nDirection \" + dX + \", \" + dY;\n //\n // Compute intersection\n\n var res = {}; // Case: Vertical direction (up)\n\n if (0 === dX && 0 < dY) {\n res.x = X; // s += \"\\nUp direction\";\n\n res.y = Y + H / 2;\n return res;\n } // Case: Vertical direction (down)\n\n\n if (0 === dX && 0 > dY) {\n res.x = X;\n res.y = Y + H / 2; // s += \"\\nDown direction\";\n\n return res;\n } // Case: Intersects the right border\n\n\n if (0 < dX && -1 * nodeSlope <= dirSlope && dirSlope <= nodeSlope) {\n res.x = X + W / 2;\n res.y = Y + W * dY / 2 / dX; // s += \"\\nRightborder\";\n\n return res;\n } // Case: Intersects the left border\n\n\n if (0 > dX && -1 * nodeSlope <= dirSlope && dirSlope <= nodeSlope) {\n res.x = X - W / 2;\n res.y = Y - W * dY / 2 / dX; // s += \"\\nLeftborder\";\n\n return res;\n } // Case: Intersects the top border\n\n\n if (0 < dY && (dirSlope <= -1 * nodeSlope || dirSlope >= nodeSlope)) {\n res.x = X + H * dX / 2 / dY;\n res.y = Y + H / 2; // s += \"\\nTop border\";\n\n return res;\n } // Case: Intersects the bottom border\n\n\n if (0 > dY && (dirSlope <= -1 * nodeSlope || dirSlope >= nodeSlope)) {\n res.x = X - H * dX / 2 / dY;\n res.y = Y - H / 2; // s += \"\\nBottom border\";\n\n return res;\n } // s += \"\\nClipping point found at \" + res.x + \", \" + res.y;\n // logDebug(s);\n\n\n return res;\n};\n/**\n * @brief : Calculates all edge forces\n */\n\n\nvar calculateEdgeForces = function calculateEdgeForces(layoutInfo, options) {\n // Iterate over all edges\n for (var i = 0; i < layoutInfo.edgeSize; i++) {\n // Get edge, source & target nodes\n var edge = layoutInfo.layoutEdges[i];\n var sourceIx = layoutInfo.idToIndex[edge.sourceId];\n var source = layoutInfo.layoutNodes[sourceIx];\n var targetIx = layoutInfo.idToIndex[edge.targetId];\n var target = layoutInfo.layoutNodes[targetIx]; // Get direction of line connecting both node centers\n\n var directionX = target.positionX - source.positionX;\n var directionY = target.positionY - source.positionY; // If both centers are the same, do nothing.\n // A random force has already been applied as node repulsion\n\n if (0 === directionX && 0 === directionY) {\n continue;\n } // Get clipping points for both nodes\n\n\n var point1 = findClippingPoint(source, directionX, directionY);\n var point2 = findClippingPoint(target, -1 * directionX, -1 * directionY);\n var lx = point2.x - point1.x;\n var ly = point2.y - point1.y;\n var l = Math.sqrt(lx * lx + ly * ly);\n var force = Math.pow(edge.idealLength - l, 2) / edge.elasticity;\n\n if (0 !== l) {\n var forceX = force * lx / l;\n var forceY = force * ly / l;\n } else {\n var forceX = 0;\n var forceY = 0;\n } // Add this force to target and source nodes\n\n\n if (!source.isLocked) {\n source.offsetX += forceX;\n source.offsetY += forceY;\n }\n\n if (!target.isLocked) {\n target.offsetX -= forceX;\n target.offsetY -= forceY;\n } // var s = 'Edge force between nodes ' + source.id + ' and ' + target.id;\n // s += \"\\nDistance: \" + l + \" Force: (\" + forceX + \", \" + forceY + \")\";\n // logDebug(s);\n\n }\n};\n/**\n * @brief : Computes gravity forces for all nodes\n */\n\n\nvar calculateGravityForces = function calculateGravityForces(layoutInfo, options) {\n var distThreshold = 1; // var s = 'calculateGravityForces';\n // logDebug(s);\n\n for (var i = 0; i < layoutInfo.graphSet.length; i++) {\n var graph = layoutInfo.graphSet[i];\n var numNodes = graph.length; // s = \"Set: \" + graph.toString();\n // logDebug(s);\n // Compute graph center\n\n if (0 === i) {\n var centerX = layoutInfo.clientHeight / 2;\n var centerY = layoutInfo.clientWidth / 2;\n } else {\n // Get Parent node for this graph, and use its position as center\n var temp = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[0]]];\n var parent = layoutInfo.layoutNodes[layoutInfo.idToIndex[temp.parentId]];\n var centerX = parent.positionX;\n var centerY = parent.positionY;\n } // s = \"Center found at: \" + centerX + \", \" + centerY;\n // logDebug(s);\n // Apply force to all nodes in graph\n\n\n for (var j = 0; j < numNodes; j++) {\n var node = layoutInfo.layoutNodes[layoutInfo.idToIndex[graph[j]]]; // s = \"Node: \" + node.id;\n\n if (node.isLocked) {\n continue;\n }\n\n var dx = centerX - node.positionX;\n var dy = centerY - node.positionY;\n var d = Math.sqrt(dx * dx + dy * dy);\n\n if (d > distThreshold) {\n var fx = options.gravity * dx / d;\n var fy = options.gravity * dy / d;\n node.offsetX += fx;\n node.offsetY += fy; // s += \": Applied force: \" + fx + \", \" + fy;\n } // s += \": skypped since it's too close to center\";\n // logDebug(s);\n\n }\n }\n};\n/**\n * @brief : This function propagates the existing offsets from\n * parent nodes to its descendents.\n * @arg layoutInfo : layoutInfo Object\n * @arg cy : cytoscape Object\n * @arg options : Layout options\n */\n\n\nvar propagateForces = function propagateForces(layoutInfo, options) {\n // Inline implementation of a queue, used for traversing the graph in BFS order\n var queue = [];\n var start = 0; // Points to the start the queue\n\n var end = -1; // Points to the end of the queue\n // logDebug('propagateForces');\n // Start by visiting the nodes in the root graph\n\n queue.push.apply(queue, layoutInfo.graphSet[0]);\n end += layoutInfo.graphSet[0].length; // Traverse the graph, level by level,\n\n while (start <= end) {\n // Get the node to visit and remove it from queue\n var nodeId = queue[start++];\n var nodeIndex = layoutInfo.idToIndex[nodeId];\n var node = layoutInfo.layoutNodes[nodeIndex];\n var children = node.children; // We only need to process the node if it's compound\n\n if (0 < children.length && !node.isLocked) {\n var offX = node.offsetX;\n var offY = node.offsetY; // var s = \"Propagating offset from parent node : \" + node.id +\n // \". OffsetX: \" + offX + \". OffsetY: \" + offY;\n // s += \"\\n Children: \" + children.toString();\n // logDebug(s);\n\n for (var i = 0; i < children.length; i++) {\n var childNode = layoutInfo.layoutNodes[layoutInfo.idToIndex[children[i]]]; // Propagate offset\n\n childNode.offsetX += offX;\n childNode.offsetY += offY; // Add children to queue to be visited\n\n queue[++end] = children[i];\n } // Reset parent offsets\n\n\n node.offsetX = 0;\n node.offsetY = 0;\n }\n }\n};\n/**\n * @brief : Updates the layout model positions, based on\n * the accumulated forces\n */\n\n\nvar updatePositions = function updatePositions(layoutInfo, options) {\n // var s = 'Updating positions';\n // logDebug(s);\n // Reset boundaries for compound nodes\n for (var i = 0; i < layoutInfo.nodeSize; i++) {\n var n = layoutInfo.layoutNodes[i];\n\n if (0 < n.children.length) {\n // logDebug(\"Resetting boundaries of compound node: \" + n.id);\n n.maxX = undefined;\n n.minX = undefined;\n n.maxY = undefined;\n n.minY = undefined;\n }\n }\n\n for (var i = 0; i < layoutInfo.nodeSize; i++) {\n var n = layoutInfo.layoutNodes[i];\n\n if (0 < n.children.length || n.isLocked) {\n // No need to set compound or locked node position\n // logDebug(\"Skipping position update of node: \" + n.id);\n continue;\n } // s = \"Node: \" + n.id + \" Previous position: (\" +\n // n.positionX + \", \" + n.positionY + \").\";\n // Limit displacement in order to improve stability\n\n\n var tempForce = limitForce(n.offsetX, n.offsetY, layoutInfo.temperature);\n n.positionX += tempForce.x;\n n.positionY += tempForce.y;\n n.offsetX = 0;\n n.offsetY = 0;\n n.minX = n.positionX - n.width;\n n.maxX = n.positionX + n.width;\n n.minY = n.positionY - n.height;\n n.maxY = n.positionY + n.height; // s += \" New Position: (\" + n.positionX + \", \" + n.positionY + \").\";\n // logDebug(s);\n // Update ancestry boudaries\n\n updateAncestryBoundaries(n, layoutInfo);\n } // Update size, position of compund nodes\n\n\n for (var i = 0; i < layoutInfo.nodeSize; i++) {\n var n = layoutInfo.layoutNodes[i];\n\n if (0 < n.children.length && !n.isLocked) {\n n.positionX = (n.maxX + n.minX) / 2;\n n.positionY = (n.maxY + n.minY) / 2;\n n.width = n.maxX - n.minX;\n n.height = n.maxY - n.minY; // s = \"Updating position, size of compound node \" + n.id;\n // s += \"\\nPositionX: \" + n.positionX + \", PositionY: \" + n.positionY;\n // s += \"\\nWidth: \" + n.width + \", Height: \" + n.height;\n // logDebug(s);\n }\n }\n};\n/**\n * @brief : Limits a force (forceX, forceY) to be not\n * greater (in modulo) than max.\n 8 Preserves force direction.\n */\n\n\nvar limitForce = function limitForce(forceX, forceY, max) {\n // var s = \"Limiting force: (\" + forceX + \", \" + forceY + \"). Max: \" + max;\n var force = Math.sqrt(forceX * forceX + forceY * forceY);\n\n if (force > max) {\n var res = {\n x: max * forceX / force,\n y: max * forceY / force\n };\n } else {\n var res = {\n x: forceX,\n y: forceY\n };\n } // s += \".\\nResult: (\" + res.x + \", \" + res.y + \")\";\n // logDebug(s);\n\n\n return res;\n};\n/**\n * @brief : Function used for keeping track of compound node\n * sizes, since they should bound all their subnodes.\n */\n\n\nvar updateAncestryBoundaries = function updateAncestryBoundaries(node, layoutInfo) {\n // var s = \"Propagating new position/size of node \" + node.id;\n var parentId = node.parentId;\n\n if (null == parentId) {\n // If there's no parent, we are done\n // s += \". No parent node.\";\n // logDebug(s);\n return;\n } // Get Parent Node\n\n\n var p = layoutInfo.layoutNodes[layoutInfo.idToIndex[parentId]];\n var flag = false; // MaxX\n\n if (null == p.maxX || node.maxX + p.padRight > p.maxX) {\n p.maxX = node.maxX + p.padRight;\n flag = true; // s += \"\\nNew maxX for parent node \" + p.id + \": \" + p.maxX;\n } // MinX\n\n\n if (null == p.minX || node.minX - p.padLeft < p.minX) {\n p.minX = node.minX - p.padLeft;\n flag = true; // s += \"\\nNew minX for parent node \" + p.id + \": \" + p.minX;\n } // MaxY\n\n\n if (null == p.maxY || node.maxY + p.padBottom > p.maxY) {\n p.maxY = node.maxY + p.padBottom;\n flag = true; // s += \"\\nNew maxY for parent node \" + p.id + \": \" + p.maxY;\n } // MinY\n\n\n if (null == p.minY || node.minY - p.padTop < p.minY) {\n p.minY = node.minY - p.padTop;\n flag = true; // s += \"\\nNew minY for parent node \" + p.id + \": \" + p.minY;\n } // If updated boundaries, propagate changes upward\n\n\n if (flag) {\n // logDebug(s);\n return updateAncestryBoundaries(p, layoutInfo);\n } // s += \". No changes in boundaries/position of parent node \" + p.id;\n // logDebug(s);\n\n\n return;\n};\n\nvar separateComponents = function separateComponents(layoutInfo, options) {\n var nodes = layoutInfo.layoutNodes;\n var components = [];\n\n for (var i = 0; i < nodes.length; i++) {\n var node = nodes[i];\n var cid = node.cmptId;\n var component = components[cid] = components[cid] || [];\n component.push(node);\n }\n\n var totalA = 0;\n\n for (var i = 0; i < components.length; i++) {\n var c = components[i];\n\n if (!c) {\n continue;\n }\n\n c.x1 = Infinity;\n c.x2 = -Infinity;\n c.y1 = Infinity;\n c.y2 = -Infinity;\n\n for (var j = 0; j < c.length; j++) {\n var n = c[j];\n c.x1 = Math.min(c.x1, n.positionX - n.width / 2);\n c.x2 = Math.max(c.x2, n.positionX + n.width / 2);\n c.y1 = Math.min(c.y1, n.positionY - n.height / 2);\n c.y2 = Math.max(c.y2, n.positionY + n.height / 2);\n }\n\n c.w = c.x2 - c.x1;\n c.h = c.y2 - c.y1;\n totalA += c.w * c.h;\n }\n\n components.sort(function (c1, c2) {\n return c2.w * c2.h - c1.w * c1.h;\n });\n var x = 0;\n var y = 0;\n var usedW = 0;\n var rowH = 0;\n var maxRowW = Math.sqrt(totalA) * layoutInfo.clientWidth / layoutInfo.clientHeight;\n\n for (var i = 0; i < components.length; i++) {\n var c = components[i];\n\n if (!c) {\n continue;\n }\n\n for (var j = 0; j < c.length; j++) {\n var n = c[j];\n\n if (!n.isLocked) {\n n.positionX += x - c.x1;\n n.positionY += y - c.y1;\n }\n }\n\n x += c.w + options.componentSpacing;\n usedW += c.w + options.componentSpacing;\n rowH = Math.max(rowH, c.h);\n\n if (usedW > maxRowW) {\n y += rowH + options.componentSpacing;\n x = 0;\n usedW = 0;\n rowH = 0;\n }\n }\n};\n\nvar defaults$d = {\n fit: true,\n // whether to fit the viewport to the graph\n padding: 30,\n // padding used on fit\n boundingBox: undefined,\n // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n avoidOverlap: true,\n // prevents node overlap, may overflow boundingBox if not enough space\n avoidOverlapPadding: 10,\n // extra spacing around nodes when avoidOverlap: true\n nodeDimensionsIncludeLabels: false,\n // Excludes the label when calculating node bounding boxes for the layout algorithm\n spacingFactor: undefined,\n // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up\n condense: false,\n // uses all available space on false, uses minimal space on true\n rows: undefined,\n // force num of rows in the grid\n cols: undefined,\n // force num of columns in the grid\n position: function position(node) {},\n // returns { row, col } for element\n sort: undefined,\n // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }\n animate: false,\n // whether to transition the node positions\n animationDuration: 500,\n // duration of animation in ms if enabled\n animationEasing: undefined,\n // easing of animation if enabled\n animateFilter: function animateFilter(node, i) {\n return true;\n },\n // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts\n ready: undefined,\n // callback on layoutready\n stop: undefined,\n // callback on layoutstop\n transform: function transform(node, position) {\n return position;\n } // transform a given node position. Useful for changing flow direction in discrete layouts \n\n};\n\nfunction GridLayout(options) {\n this.options = extend({}, defaults$d, options);\n}\n\nGridLayout.prototype.run = function () {\n var params = this.options;\n var options = params;\n var cy = params.cy;\n var eles = options.eles;\n var nodes = eles.nodes().not(':parent');\n\n if (options.sort) {\n nodes = nodes.sort(options.sort);\n }\n\n var bb = makeBoundingBox(options.boundingBox ? options.boundingBox : {\n x1: 0,\n y1: 0,\n w: cy.width(),\n h: cy.height()\n });\n\n if (bb.h === 0 || bb.w === 0) {\n eles.nodes().layoutPositions(this, options, function (ele) {\n return {\n x: bb.x1,\n y: bb.y1\n };\n });\n } else {\n // width/height * splits^2 = cells where splits is number of times to split width\n var cells = nodes.size();\n var splits = Math.sqrt(cells * bb.h / bb.w);\n var rows = Math.round(splits);\n var cols = Math.round(bb.w / bb.h * splits);\n\n var small = function small(val) {\n if (val == null) {\n return Math.min(rows, cols);\n } else {\n var min = Math.min(rows, cols);\n\n if (min == rows) {\n rows = val;\n } else {\n cols = val;\n }\n }\n };\n\n var large = function large(val) {\n if (val == null) {\n return Math.max(rows, cols);\n } else {\n var max = Math.max(rows, cols);\n\n if (max == rows) {\n rows = val;\n } else {\n cols = val;\n }\n }\n };\n\n var oRows = options.rows;\n var oCols = options.cols != null ? options.cols : options.columns; // if rows or columns were set in options, use those values\n\n if (oRows != null && oCols != null) {\n rows = oRows;\n cols = oCols;\n } else if (oRows != null && oCols == null) {\n rows = oRows;\n cols = Math.ceil(cells / rows);\n } else if (oRows == null && oCols != null) {\n cols = oCols;\n rows = Math.ceil(cells / cols);\n } // otherwise use the automatic values and adjust accordingly\n // if rounding was up, see if we can reduce rows or columns\n else if (cols * rows > cells) {\n var sm = small();\n var lg = large(); // reducing the small side takes away the most cells, so try it first\n\n if ((sm - 1) * lg >= cells) {\n small(sm - 1);\n } else if ((lg - 1) * sm >= cells) {\n large(lg - 1);\n }\n } else {\n // if rounding was too low, add rows or columns\n while (cols * rows < cells) {\n var _sm = small();\n\n var _lg = large(); // try to add to larger side first (adds less in multiplication)\n\n\n if ((_lg + 1) * _sm >= cells) {\n large(_lg + 1);\n } else {\n small(_sm + 1);\n }\n }\n }\n\n var cellWidth = bb.w / cols;\n var cellHeight = bb.h / rows;\n\n if (options.condense) {\n cellWidth = 0;\n cellHeight = 0;\n }\n\n if (options.avoidOverlap) {\n for (var i = 0; i < nodes.length; i++) {\n var node = nodes[i];\n var pos = node._private.position;\n\n if (pos.x == null || pos.y == null) {\n // for bb\n pos.x = 0;\n pos.y = 0;\n }\n\n var nbb = node.layoutDimensions(options);\n var p = options.avoidOverlapPadding;\n var w = nbb.w + p;\n var h = nbb.h + p;\n cellWidth = Math.max(cellWidth, w);\n cellHeight = Math.max(cellHeight, h);\n }\n }\n\n var cellUsed = {}; // e.g. 'c-0-2' => true\n\n var used = function used(row, col) {\n return cellUsed['c-' + row + '-' + col] ? true : false;\n };\n\n var use = function use(row, col) {\n cellUsed['c-' + row + '-' + col] = true;\n }; // to keep track of current cell position\n\n\n var row = 0;\n var col = 0;\n\n var moveToNextCell = function moveToNextCell() {\n col++;\n\n if (col >= cols) {\n col = 0;\n row++;\n }\n }; // get a cache of all the manual positions\n\n\n var id2manPos = {};\n\n for (var _i = 0; _i < nodes.length; _i++) {\n var _node = nodes[_i];\n var rcPos = options.position(_node);\n\n if (rcPos && (rcPos.row !== undefined || rcPos.col !== undefined)) {\n // must have at least row or col def'd\n var _pos = {\n row: rcPos.row,\n col: rcPos.col\n };\n\n if (_pos.col === undefined) {\n // find unused col\n _pos.col = 0;\n\n while (used(_pos.row, _pos.col)) {\n _pos.col++;\n }\n } else if (_pos.row === undefined) {\n // find unused row\n _pos.row = 0;\n\n while (used(_pos.row, _pos.col)) {\n _pos.row++;\n }\n }\n\n id2manPos[_node.id()] = _pos;\n use(_pos.row, _pos.col);\n }\n }\n\n var getPos = function getPos(element, i) {\n var x, y;\n\n if (element.locked() || element.isParent()) {\n return false;\n } // see if we have a manual position set\n\n\n var rcPos = id2manPos[element.id()];\n\n if (rcPos) {\n x = rcPos.col * cellWidth + cellWidth / 2 + bb.x1;\n y = rcPos.row * cellHeight + cellHeight / 2 + bb.y1;\n } else {\n // otherwise set automatically\n while (used(row, col)) {\n moveToNextCell();\n }\n\n x = col * cellWidth + cellWidth / 2 + bb.x1;\n y = row * cellHeight + cellHeight / 2 + bb.y1;\n use(row, col);\n moveToNextCell();\n }\n\n return {\n x: x,\n y: y\n };\n };\n\n nodes.layoutPositions(this, options, getPos);\n }\n\n return this; // chaining\n};\n\nvar defaults$e = {\n ready: function ready() {},\n // on layoutready\n stop: function stop() {} // on layoutstop\n\n}; // constructor\n// options : object containing layout options\n\nfunction NullLayout(options) {\n this.options = extend({}, defaults$e, options);\n} // runs the layout\n\n\nNullLayout.prototype.run = function () {\n var options = this.options;\n var eles = options.eles; // elements to consider in the layout\n\n var layout = this; // cy is automatically populated for us in the constructor\n // (disable eslint for next line as this serves as example layout code to external developers)\n // eslint-disable-next-line no-unused-vars\n\n var cy = options.cy;\n layout.emit('layoutstart'); // puts all nodes at (0, 0)\n // n.b. most layouts would use layoutPositions(), instead of positions() and manual events\n\n eles.nodes().positions(function () {\n return {\n x: 0,\n y: 0\n };\n }); // trigger layoutready when each node has had its position set at least once\n\n layout.one('layoutready', options.ready);\n layout.emit('layoutready'); // trigger layoutstop when the layout stops (e.g. finishes)\n\n layout.one('layoutstop', options.stop);\n layout.emit('layoutstop');\n return this; // chaining\n}; // called on continuous layouts to stop them before they finish\n\n\nNullLayout.prototype.stop = function () {\n return this; // chaining\n};\n\nvar defaults$f = {\n positions: undefined,\n // map of (node id) => (position obj); or function(node){ return somPos; }\n zoom: undefined,\n // the zoom level to set (prob want fit = false if set)\n pan: undefined,\n // the pan level to set (prob want fit = false if set)\n fit: true,\n // whether to fit to viewport\n padding: 30,\n // padding on fit\n animate: false,\n // whether to transition the node positions\n animationDuration: 500,\n // duration of animation in ms if enabled\n animationEasing: undefined,\n // easing of animation if enabled\n animateFilter: function animateFilter(node, i) {\n return true;\n },\n // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts\n ready: undefined,\n // callback on layoutready\n stop: undefined,\n // callback on layoutstop\n transform: function transform(node, position) {\n return position;\n } // transform a given node position. Useful for changing flow direction in discrete layouts\n\n};\n\nfunction PresetLayout(options) {\n this.options = extend({}, defaults$f, options);\n}\n\nPresetLayout.prototype.run = function () {\n var options = this.options;\n var eles = options.eles;\n var nodes = eles.nodes();\n var posIsFn = fn(options.positions);\n\n function getPosition(node) {\n if (options.positions == null) {\n return copyPosition(node.position());\n }\n\n if (posIsFn) {\n return options.positions(node);\n }\n\n var pos = options.positions[node._private.data.id];\n\n if (pos == null) {\n return null;\n }\n\n return pos;\n }\n\n nodes.layoutPositions(this, options, function (node, i) {\n var position = getPosition(node);\n\n if (node.locked() || position == null) {\n return false;\n }\n\n return position;\n });\n return this; // chaining\n};\n\nvar defaults$g = {\n fit: true,\n // whether to fit to viewport\n padding: 30,\n // fit padding\n boundingBox: undefined,\n // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }\n animate: false,\n // whether to transition the node positions\n animationDuration: 500,\n // duration of animation in ms if enabled\n animationEasing: undefined,\n // easing of animation if enabled\n animateFilter: function animateFilter(node, i) {\n return true;\n },\n // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts\n ready: undefined,\n // callback on layoutready\n stop: undefined,\n // callback on layoutstop\n transform: function transform(node, position) {\n return position;\n } // transform a given node position. Useful for changing flow direction in discrete layouts \n\n};\n\nfunction RandomLayout(options) {\n this.options = extend({}, defaults$g, options);\n}\n\nRandomLayout.prototype.run = function () {\n var options = this.options;\n var cy = options.cy;\n var eles = options.eles;\n var bb = makeBoundingBox(options.boundingBox ? options.boundingBox : {\n x1: 0,\n y1: 0,\n w: cy.width(),\n h: cy.height()\n });\n\n var getPos = function getPos(node, i) {\n return {\n x: bb.x1 + Math.round(Math.random() * bb.w),\n y: bb.y1 + Math.round(Math.random() * bb.h)\n };\n };\n\n eles.nodes().layoutPositions(this, options, getPos);\n return this; // chaining\n};\n\nvar layout = [{\n name: 'breadthfirst',\n impl: BreadthFirstLayout\n}, {\n name: 'circle',\n impl: CircleLayout\n}, {\n name: 'concentric',\n impl: ConcentricLayout\n}, {\n name: 'cose',\n impl: CoseLayout\n}, {\n name: 'grid',\n impl: GridLayout\n}, {\n name: 'null',\n impl: NullLayout\n}, {\n name: 'preset',\n impl: PresetLayout\n}, {\n name: 'random',\n impl: RandomLayout\n}];\n\nfunction NullRenderer(options) {\n this.options = options;\n this.notifications = 0; // for testing\n}\n\nvar noop$1 = function noop() {};\n\nvar throwImgErr = function throwImgErr() {\n throw new Error('A headless instance can not render images');\n};\n\nNullRenderer.prototype = {\n recalculateRenderedStyle: noop$1,\n notify: function notify() {\n this.notifications++;\n },\n init: noop$1,\n isHeadless: function isHeadless() {\n return true;\n },\n png: throwImgErr,\n jpg: throwImgErr\n};\n\nvar BRp = {};\nBRp.arrowShapeWidth = 0.3;\n\nBRp.registerArrowShapes = function () {\n var arrowShapes = this.arrowShapes = {};\n var renderer = this; // Contract for arrow shapes:\n // 0, 0 is arrow tip\n // (0, 1) is direction towards node\n // (1, 0) is right\n //\n // functional api:\n // collide: check x, y in shape\n // roughCollide: called before collide, no false negatives\n // draw: draw\n // spacing: dist(arrowTip, nodeBoundary)\n // gap: dist(edgeTip, nodeBoundary), edgeTip may != arrowTip\n\n var bbCollide = function bbCollide(x, y, size, angle, translation, edgeWidth, padding) {\n var x1 = translation.x - size / 2 - padding;\n var x2 = translation.x + size / 2 + padding;\n var y1 = translation.y - size / 2 - padding;\n var y2 = translation.y + size / 2 + padding;\n var inside = x1 <= x && x <= x2 && y1 <= y && y <= y2;\n return inside;\n };\n\n var transform = function transform(x, y, size, angle, translation) {\n var xRotated = x * Math.cos(angle) - y * Math.sin(angle);\n var yRotated = x * Math.sin(angle) + y * Math.cos(angle);\n var xScaled = xRotated * size;\n var yScaled = yRotated * size;\n var xTranslated = xScaled + translation.x;\n var yTranslated = yScaled + translation.y;\n return {\n x: xTranslated,\n y: yTranslated\n };\n };\n\n var transformPoints = function transformPoints(pts, size, angle, translation) {\n var retPts = [];\n\n for (var i = 0; i < pts.length; i += 2) {\n var x = pts[i];\n var y = pts[i + 1];\n retPts.push(transform(x, y, size, angle, translation));\n }\n\n return retPts;\n };\n\n var pointsToArr = function pointsToArr(pts) {\n var ret = [];\n\n for (var i = 0; i < pts.length; i++) {\n var p = pts[i];\n ret.push(p.x, p.y);\n }\n\n return ret;\n };\n\n var standardGap = function standardGap(edge) {\n return edge.pstyle('width').pfValue * edge.pstyle('arrow-scale').pfValue * 2;\n };\n\n var defineArrowShape = function defineArrowShape(name, defn) {\n if (string(defn)) {\n defn = arrowShapes[defn];\n }\n\n arrowShapes[name] = extend({\n name: name,\n points: [-0.15, -0.3, 0.15, -0.3, 0.15, 0.3, -0.15, 0.3],\n collide: function collide(x, y, size, angle, translation, padding) {\n var points = pointsToArr(transformPoints(this.points, size + 2 * padding, angle, translation));\n var inside = pointInsidePolygonPoints(x, y, points);\n return inside;\n },\n roughCollide: bbCollide,\n draw: function draw(context, size, angle, translation) {\n var points = transformPoints(this.points, size, angle, translation);\n renderer.arrowShapeImpl('polygon')(context, points);\n },\n spacing: function spacing(edge) {\n return 0;\n },\n gap: standardGap\n }, defn);\n };\n\n defineArrowShape('none', {\n collide: falsify,\n roughCollide: falsify,\n draw: noop,\n spacing: zeroify,\n gap: zeroify\n });\n defineArrowShape('triangle', {\n points: [-0.15, -0.3, 0, 0, 0.15, -0.3]\n });\n defineArrowShape('arrow', 'triangle');\n defineArrowShape('triangle-backcurve', {\n points: arrowShapes['triangle'].points,\n controlPoint: [0, -0.15],\n roughCollide: bbCollide,\n draw: function draw(context, size, angle, translation, edgeWidth) {\n var ptsTrans = transformPoints(this.points, size, angle, translation);\n var ctrlPt = this.controlPoint;\n var ctrlPtTrans = transform(ctrlPt[0], ctrlPt[1], size, angle, translation);\n renderer.arrowShapeImpl(this.name)(context, ptsTrans, ctrlPtTrans);\n },\n gap: function gap(edge) {\n return standardGap(edge) * 0.8;\n }\n });\n defineArrowShape('triangle-tee', {\n points: [0, 0, 0.15, -0.3, -0.15, -0.3, 0, 0],\n pointsTee: [-0.15, -0.4, -0.15, -0.5, 0.15, -0.5, 0.15, -0.4],\n collide: function collide(x, y, size, angle, translation, edgeWidth, padding) {\n var triPts = pointsToArr(transformPoints(this.points, size + 2 * padding, angle, translation));\n var teePts = pointsToArr(transformPoints(this.pointsTee, size + 2 * padding, angle, translation));\n var inside = pointInsidePolygonPoints(x, y, triPts) || pointInsidePolygonPoints(x, y, teePts);\n return inside;\n },\n draw: function draw(context, size, angle, translation, edgeWidth) {\n var triPts = transformPoints(this.points, size, angle, translation);\n var teePts = transformPoints(this.pointsTee, size, angle, translation);\n renderer.arrowShapeImpl(this.name)(context, triPts, teePts);\n }\n });\n defineArrowShape('circle-triangle', {\n radius: 0.15,\n pointsTr: [0, -0.15, 0.15, -0.45, -0.15, -0.45, 0, -0.15],\n collide: function collide(x, y, size, angle, translation, edgeWidth, padding) {\n var t = translation;\n var circleInside = Math.pow(t.x - x, 2) + Math.pow(t.y - y, 2) <= Math.pow((size + 2 * padding) * this.radius, 2);\n var triPts = pointsToArr(transformPoints(this.points, size + 2 * padding, angle, translation));\n return pointInsidePolygonPoints(x, y, triPts) || circleInside;\n },\n draw: function draw(context, size, angle, translation, edgeWidth) {\n var triPts = transformPoints(this.pointsTr, size, angle, translation);\n renderer.arrowShapeImpl(this.name)(context, triPts, translation.x, translation.y, this.radius * size);\n },\n spacing: function spacing(edge) {\n return renderer.getArrowWidth(edge.pstyle('width').pfValue, edge.pstyle('arrow-scale').value) * this.radius;\n }\n });\n defineArrowShape('triangle-cross', {\n points: [0, 0, 0.15, -0.3, -0.15, -0.3, 0, 0],\n baseCrossLinePts: [-0.15, -0.4, // first half of the rectangle\n -0.15, -0.4, 0.15, -0.4, // second half of the rectangle\n 0.15, -0.4],\n crossLinePts: function crossLinePts(size, edgeWidth) {\n // shift points so that the distance between the cross points matches edge width\n var p = this.baseCrossLinePts.slice();\n var shiftFactor = edgeWidth / size;\n var y0 = 3;\n var y1 = 5;\n p[y0] = p[y0] - shiftFactor;\n p[y1] = p[y1] - shiftFactor;\n return p;\n },\n collide: function collide(x, y, size, angle, translation, edgeWidth, padding) {\n var triPts = pointsToArr(transformPoints(this.points, size + 2 * padding, angle, translation));\n var teePts = pointsToArr(transformPoints(this.crossLinePts(size, edgeWidth), size + 2 * padding, angle, translation));\n var inside = pointInsidePolygonPoints(x, y, triPts) || pointInsidePolygonPoints(x, y, teePts);\n return inside;\n },\n draw: function draw(context, size, angle, translation, edgeWidth) {\n var triPts = transformPoints(this.points, size, angle, translation);\n var crossLinePts = transformPoints(this.crossLinePts(size, edgeWidth), size, angle, translation);\n renderer.arrowShapeImpl(this.name)(context, triPts, crossLinePts);\n }\n });\n defineArrowShape('vee', {\n points: [-0.15, -0.3, 0, 0, 0.15, -0.3, 0, -0.15],\n gap: function gap(edge) {\n return standardGap(edge) * 0.525;\n }\n });\n defineArrowShape('circle', {\n radius: 0.15,\n collide: function collide(x, y, size, angle, translation, edgeWidth, padding) {\n var t = translation;\n var inside = Math.pow(t.x - x, 2) + Math.pow(t.y - y, 2) <= Math.pow((size + 2 * padding) * this.radius, 2);\n return inside;\n },\n draw: function draw(context, size, angle, translation, edgeWidth) {\n renderer.arrowShapeImpl(this.name)(context, translation.x, translation.y, this.radius * size);\n },\n spacing: function spacing(edge) {\n return renderer.getArrowWidth(edge.pstyle('width').pfValue, edge.pstyle('arrow-scale').value) * this.radius;\n }\n });\n defineArrowShape('tee', {\n points: [-0.15, 0, -0.15, -0.1, 0.15, -0.1, 0.15, 0],\n spacing: function spacing(edge) {\n return 1;\n },\n gap: function gap(edge) {\n return 1;\n }\n });\n defineArrowShape('square', {\n points: [-0.15, 0.00, 0.15, 0.00, 0.15, -0.3, -0.15, -0.3]\n });\n defineArrowShape('diamond', {\n points: [-0.15, -0.15, 0, -0.3, 0.15, -0.15, 0, 0],\n gap: function gap(edge) {\n return edge.pstyle('width').pfValue * edge.pstyle('arrow-scale').value;\n }\n });\n defineArrowShape('chevron', {\n points: [0, 0, -0.15, -0.15, -0.1, -0.2, 0, -0.1, 0.1, -0.2, 0.15, -0.15],\n gap: function gap(edge) {\n return 0.95 * edge.pstyle('width').pfValue * edge.pstyle('arrow-scale').value;\n }\n });\n};\n\nvar BRp$1 = {}; // Project mouse\n\nBRp$1.projectIntoViewport = function (clientX, clientY) {\n var cy = this.cy;\n var offsets = this.findContainerClientCoords();\n var offsetLeft = offsets[0];\n var offsetTop = offsets[1];\n var scale = offsets[4];\n var pan = cy.pan();\n var zoom = cy.zoom();\n var x = ((clientX - offsetLeft) / scale - pan.x) / zoom;\n var y = ((clientY - offsetTop) / scale - pan.y) / zoom;\n return [x, y];\n};\n\nBRp$1.findContainerClientCoords = function () {\n if (this.containerBB) {\n return this.containerBB;\n }\n\n var container = this.container;\n var rect = container.getBoundingClientRect();\n var style = window$1.getComputedStyle(container);\n\n var styleValue = function styleValue(name) {\n return parseFloat(style.getPropertyValue(name));\n };\n\n var padding = {\n left: styleValue('padding-left'),\n right: styleValue('padding-right'),\n top: styleValue('padding-top'),\n bottom: styleValue('padding-bottom')\n };\n var border = {\n left: styleValue('border-left-width'),\n right: styleValue('border-right-width'),\n top: styleValue('border-top-width'),\n bottom: styleValue('border-bottom-width')\n };\n var clientWidth = container.clientWidth;\n var clientHeight = container.clientHeight;\n var paddingHor = padding.left + padding.right;\n var paddingVer = padding.top + padding.bottom;\n var borderHor = border.left + border.right;\n var scale = rect.width / (clientWidth + borderHor);\n var unscaledW = clientWidth - paddingHor;\n var unscaledH = clientHeight - paddingVer;\n var left = rect.left + padding.left + border.left;\n var top = rect.top + padding.top + border.top;\n return this.containerBB = [left, top, unscaledW, unscaledH, scale];\n};\n\nBRp$1.invalidateContainerClientCoordsCache = function () {\n this.containerBB = null;\n};\n\nBRp$1.findNearestElement = function (x, y, interactiveElementsOnly, isTouch) {\n return this.findNearestElements(x, y, interactiveElementsOnly, isTouch)[0];\n};\n\nBRp$1.findNearestElements = function (x, y, interactiveElementsOnly, isTouch) {\n var self = this;\n var r = this;\n var eles = r.getCachedZSortedEles();\n var near = []; // 1 node max, 1 edge max\n\n var zoom = r.cy.zoom();\n var hasCompounds = r.cy.hasCompoundNodes();\n var edgeThreshold = (isTouch ? 24 : 8) / zoom;\n var nodeThreshold = (isTouch ? 8 : 2) / zoom;\n var labelThreshold = (isTouch ? 8 : 2) / zoom;\n var minSqDist = Infinity;\n var nearEdge;\n var nearNode;\n\n if (interactiveElementsOnly) {\n eles = eles.interactive;\n }\n\n function addEle(ele, sqDist) {\n if (ele.isNode()) {\n if (nearNode) {\n return; // can't replace node\n } else {\n nearNode = ele;\n near.push(ele);\n }\n }\n\n if (ele.isEdge() && (sqDist == null || sqDist < minSqDist)) {\n if (nearEdge) {\n // then replace existing edge\n // can replace only if same z-index\n if (nearEdge.pstyle('z-compound-depth').value === ele.pstyle('z-compound-depth').value && nearEdge.pstyle('z-compound-depth').value === ele.pstyle('z-compound-depth').value) {\n for (var i = 0; i < near.length; i++) {\n if (near[i].isEdge()) {\n near[i] = ele;\n nearEdge = ele;\n minSqDist = sqDist != null ? sqDist : minSqDist;\n break;\n }\n }\n }\n } else {\n near.push(ele);\n nearEdge = ele;\n minSqDist = sqDist != null ? sqDist : minSqDist;\n }\n }\n }\n\n function checkNode(node) {\n var width = node.outerWidth() + 2 * nodeThreshold;\n var height = node.outerHeight() + 2 * nodeThreshold;\n var hw = width / 2;\n var hh = height / 2;\n var pos = node.position();\n\n if (pos.x - hw <= x && x <= pos.x + hw // bb check x\n && pos.y - hh <= y && y <= pos.y + hh // bb check y\n ) {\n var shape = r.nodeShapes[self.getNodeShape(node)];\n\n if (shape.checkPoint(x, y, 0, width, height, pos.x, pos.y)) {\n addEle(node, 0);\n return true;\n }\n }\n }\n\n function checkEdge(edge) {\n var _p = edge._private;\n var rs = _p.rscratch;\n var styleWidth = edge.pstyle('width').pfValue;\n var scale = edge.pstyle('arrow-scale').value;\n var width = styleWidth / 2 + edgeThreshold; // more like a distance radius from centre\n\n var widthSq = width * width;\n var width2 = width * 2;\n var src = _p.source;\n var tgt = _p.target;\n var sqDist;\n\n if (rs.edgeType === 'segments' || rs.edgeType === 'straight' || rs.edgeType === 'haystack') {\n var pts = rs.allpts;\n\n for (var i = 0; i + 3 < pts.length; i += 2) {\n if (inLineVicinity(x, y, pts[i], pts[i + 1], pts[i + 2], pts[i + 3], width2) && widthSq > (sqDist = sqdistToFiniteLine(x, y, pts[i], pts[i + 1], pts[i + 2], pts[i + 3]))) {\n addEle(edge, sqDist);\n return true;\n }\n }\n } else if (rs.edgeType === 'bezier' || rs.edgeType === 'multibezier' || rs.edgeType === 'self' || rs.edgeType === 'compound') {\n var pts = rs.allpts;\n\n for (var i = 0; i + 5 < rs.allpts.length; i += 4) {\n if (inBezierVicinity(x, y, pts[i], pts[i + 1], pts[i + 2], pts[i + 3], pts[i + 4], pts[i + 5], width2) && widthSq > (sqDist = sqdistToQuadraticBezier(x, y, pts[i], pts[i + 1], pts[i + 2], pts[i + 3], pts[i + 4], pts[i + 5]))) {\n addEle(edge, sqDist);\n return true;\n }\n }\n } // if we're close to the edge but didn't hit it, maybe we hit its arrows\n\n\n var src = src || _p.source;\n var tgt = tgt || _p.target;\n var arSize = self.getArrowWidth(styleWidth, scale);\n var arrows = [{\n name: 'source',\n x: rs.arrowStartX,\n y: rs.arrowStartY,\n angle: rs.srcArrowAngle\n }, {\n name: 'target',\n x: rs.arrowEndX,\n y: rs.arrowEndY,\n angle: rs.tgtArrowAngle\n }, {\n name: 'mid-source',\n x: rs.midX,\n y: rs.midY,\n angle: rs.midsrcArrowAngle\n }, {\n name: 'mid-target',\n x: rs.midX,\n y: rs.midY,\n angle: rs.midtgtArrowAngle\n }];\n\n for (var i = 0; i < arrows.length; i++) {\n var ar = arrows[i];\n var shape = r.arrowShapes[edge.pstyle(ar.name + '-arrow-shape').value];\n var edgeWidth = edge.pstyle('width').pfValue;\n\n if (shape.roughCollide(x, y, arSize, ar.angle, {\n x: ar.x,\n y: ar.y\n }, edgeWidth, edgeThreshold) && shape.collide(x, y, arSize, ar.angle, {\n x: ar.x,\n y: ar.y\n }, edgeWidth, edgeThreshold)) {\n addEle(edge);\n return true;\n }\n } // for compound graphs, hitting edge may actually want a connected node instead (b/c edge may have greater z-index precedence)\n\n\n if (hasCompounds && near.length > 0) {\n checkNode(src);\n checkNode(tgt);\n }\n }\n\n function preprop(obj, name, pre) {\n return getPrefixedProperty(obj, name, pre);\n }\n\n function checkLabel(ele, prefix) {\n var _p = ele._private;\n var th = labelThreshold;\n var prefixDash;\n\n if (prefix) {\n prefixDash = prefix + '-';\n } else {\n prefixDash = '';\n }\n\n ele.boundingBox();\n var bb = _p.labelBounds[prefix || 'main'];\n var text = ele.pstyle(prefixDash + 'label').value;\n var eventsEnabled = ele.pstyle('text-events').strValue === 'yes';\n\n if (!eventsEnabled || !text) {\n return;\n }\n\n var lx = preprop(_p.rscratch, 'labelX', prefix);\n var ly = preprop(_p.rscratch, 'labelY', prefix);\n var theta = preprop(_p.rscratch, 'labelAngle', prefix);\n var ox = ele.pstyle(prefixDash + 'text-margin-x').pfValue;\n var oy = ele.pstyle(prefixDash + 'text-margin-y').pfValue;\n var lx1 = bb.x1 - th - ox; // (-ox, -oy) as bb already includes margin\n\n var lx2 = bb.x2 + th - ox; // and rotation is about (lx, ly)\n\n var ly1 = bb.y1 - th - oy;\n var ly2 = bb.y2 + th - oy;\n\n if (theta) {\n var cos = Math.cos(theta);\n var sin = Math.sin(theta);\n\n var rotate = function rotate(x, y) {\n x = x - lx;\n y = y - ly;\n return {\n x: x * cos - y * sin + lx,\n y: x * sin + y * cos + ly\n };\n };\n\n var px1y1 = rotate(lx1, ly1);\n var px1y2 = rotate(lx1, ly2);\n var px2y1 = rotate(lx2, ly1);\n var px2y2 = rotate(lx2, ly2);\n var points = [// with the margin added after the rotation is applied\n px1y1.x + ox, px1y1.y + oy, px2y1.x + ox, px2y1.y + oy, px2y2.x + ox, px2y2.y + oy, px1y2.x + ox, px1y2.y + oy];\n\n if (pointInsidePolygonPoints(x, y, points)) {\n addEle(ele);\n return true;\n }\n } else {\n // do a cheaper bb check\n if (inBoundingBox(bb, x, y)) {\n addEle(ele);\n return true;\n }\n }\n }\n\n for (var i = eles.length - 1; i >= 0; i--) {\n // reverse order for precedence\n var ele = eles[i];\n\n if (ele.isNode()) {\n checkNode(ele) || checkLabel(ele);\n } else {\n // then edge\n checkEdge(ele) || checkLabel(ele) || checkLabel(ele, 'source') || checkLabel(ele, 'target');\n }\n }\n\n return near;\n}; // 'Give me everything from this box'\n\n\nBRp$1.getAllInBox = function (x1, y1, x2, y2) {\n var eles = this.getCachedZSortedEles().interactive;\n var box = [];\n var x1c = Math.min(x1, x2);\n var x2c = Math.max(x1, x2);\n var y1c = Math.min(y1, y2);\n var y2c = Math.max(y1, y2);\n x1 = x1c;\n x2 = x2c;\n y1 = y1c;\n y2 = y2c;\n var boxBb = makeBoundingBox({\n x1: x1,\n y1: y1,\n x2: x2,\n y2: y2\n });\n\n for (var e = 0; e < eles.length; e++) {\n var ele = eles[e];\n\n if (ele.isNode()) {\n var node = ele;\n var nodeBb = node.boundingBox({\n includeNodes: true,\n includeEdges: false,\n includeLabels: false\n });\n\n if (boundingBoxesIntersect(boxBb, nodeBb) && !boundingBoxInBoundingBox(nodeBb, boxBb)) {\n box.push(node);\n }\n } else {\n var edge = ele;\n var _p = edge._private;\n var rs = _p.rscratch;\n\n if (rs.startX != null && rs.startY != null && !inBoundingBox(boxBb, rs.startX, rs.startY)) {\n continue;\n }\n\n if (rs.endX != null && rs.endY != null && !inBoundingBox(boxBb, rs.endX, rs.endY)) {\n continue;\n }\n\n if (rs.edgeType === 'bezier' || rs.edgeType === 'multibezier' || rs.edgeType === 'self' || rs.edgeType === 'compound' || rs.edgeType === 'segments' || rs.edgeType === 'haystack') {\n var pts = _p.rstyle.bezierPts || _p.rstyle.linePts || _p.rstyle.haystackPts;\n var allInside = true;\n\n for (var i = 0; i < pts.length; i++) {\n if (!pointInBoundingBox(boxBb, pts[i])) {\n allInside = false;\n break;\n }\n }\n\n if (allInside) {\n box.push(edge);\n }\n } else if (rs.edgeType === 'haystack' || rs.edgeType === 'straight') {\n box.push(edge);\n }\n }\n }\n\n return box;\n};\n\nvar BRp$2 = {};\n\nBRp$2.calculateArrowAngles = function (edge) {\n var rs = edge._private.rscratch;\n var isHaystack = rs.edgeType === 'haystack';\n var isBezier = rs.edgeType === 'bezier';\n var isMultibezier = rs.edgeType === 'multibezier';\n var isSegments = rs.edgeType === 'segments';\n var isCompound = rs.edgeType === 'compound';\n var isSelf = rs.edgeType === 'self'; // Displacement gives direction for arrowhead orientation\n\n var dispX, dispY;\n var startX, startY, endX, endY, midX, midY;\n\n if (isHaystack) {\n startX = rs.haystackPts[0];\n startY = rs.haystackPts[1];\n endX = rs.haystackPts[2];\n endY = rs.haystackPts[3];\n } else {\n startX = rs.arrowStartX;\n startY = rs.arrowStartY;\n endX = rs.arrowEndX;\n endY = rs.arrowEndY;\n }\n\n midX = rs.midX;\n midY = rs.midY; // source\n //\n\n if (isSegments) {\n dispX = startX - rs.segpts[0];\n dispY = startY - rs.segpts[1];\n } else if (isMultibezier || isCompound || isSelf || isBezier) {\n var pts = rs.allpts;\n var bX = qbezierAt(pts[0], pts[2], pts[4], 0.1);\n var bY = qbezierAt(pts[1], pts[3], pts[5], 0.1);\n dispX = startX - bX;\n dispY = startY - bY;\n } else {\n dispX = startX - midX;\n dispY = startY - midY;\n }\n\n rs.srcArrowAngle = getAngleFromDisp(dispX, dispY); // mid target\n //\n\n var midX = rs.midX;\n var midY = rs.midY;\n\n if (isHaystack) {\n midX = (startX + endX) / 2;\n midY = (startY + endY) / 2;\n }\n\n dispX = endX - startX;\n dispY = endY - startY;\n\n if (isSegments) {\n var pts = rs.allpts;\n\n if (pts.length / 2 % 2 === 0) {\n var i2 = pts.length / 2;\n var i1 = i2 - 2;\n dispX = pts[i2] - pts[i1];\n dispY = pts[i2 + 1] - pts[i1 + 1];\n } else {\n var i2 = pts.length / 2 - 1;\n var i1 = i2 - 2;\n var i3 = i2 + 2;\n dispX = pts[i2] - pts[i1];\n dispY = pts[i2 + 1] - pts[i1 + 1];\n }\n } else if (isMultibezier || isCompound || isSelf) {\n var pts = rs.allpts;\n var cpts = rs.ctrlpts;\n var bp0x, bp0y;\n var bp1x, bp1y;\n\n if (cpts.length / 2 % 2 === 0) {\n var p0 = pts.length / 2 - 1; // startpt\n\n var ic = p0 + 2;\n var p1 = ic + 2;\n bp0x = qbezierAt(pts[p0], pts[ic], pts[p1], 0.0);\n bp0y = qbezierAt(pts[p0 + 1], pts[ic + 1], pts[p1 + 1], 0.0);\n bp1x = qbezierAt(pts[p0], pts[ic], pts[p1], 0.0001);\n bp1y = qbezierAt(pts[p0 + 1], pts[ic + 1], pts[p1 + 1], 0.0001);\n } else {\n var ic = pts.length / 2 - 1; // ctrpt\n\n var p0 = ic - 2; // startpt\n\n var p1 = ic + 2; // endpt\n\n bp0x = qbezierAt(pts[p0], pts[ic], pts[p1], 0.4999);\n bp0y = qbezierAt(pts[p0 + 1], pts[ic + 1], pts[p1 + 1], 0.4999);\n bp1x = qbezierAt(pts[p0], pts[ic], pts[p1], 0.5);\n bp1y = qbezierAt(pts[p0 + 1], pts[ic + 1], pts[p1 + 1], 0.5);\n }\n\n dispX = bp1x - bp0x;\n dispY = bp1y - bp0y;\n }\n\n rs.midtgtArrowAngle = getAngleFromDisp(dispX, dispY);\n rs.midDispX = dispX;\n rs.midDispY = dispY; // mid source\n //\n\n dispX *= -1;\n dispY *= -1;\n\n if (isSegments) {\n var pts = rs.allpts;\n\n if (pts.length / 2 % 2 === 0) ; else {\n var i2 = pts.length / 2 - 1;\n var i3 = i2 + 2;\n dispX = -(pts[i3] - pts[i2]);\n dispY = -(pts[i3 + 1] - pts[i2 + 1]);\n }\n }\n\n rs.midsrcArrowAngle = getAngleFromDisp(dispX, dispY); // target\n //\n\n if (isSegments) {\n dispX = endX - rs.segpts[rs.segpts.length - 2];\n dispY = endY - rs.segpts[rs.segpts.length - 1];\n } else if (isMultibezier || isCompound || isSelf || isBezier) {\n var pts = rs.allpts;\n var l = pts.length;\n var bX = qbezierAt(pts[l - 6], pts[l - 4], pts[l - 2], 0.9);\n var bY = qbezierAt(pts[l - 5], pts[l - 3], pts[l - 1], 0.9);\n dispX = endX - bX;\n dispY = endY - bY;\n } else {\n dispX = endX - midX;\n dispY = endY - midY;\n }\n\n rs.tgtArrowAngle = getAngleFromDisp(dispX, dispY);\n};\n\nBRp$2.getArrowWidth = BRp$2.getArrowHeight = function (edgeWidth, scale) {\n var cache = this.arrowWidthCache = this.arrowWidthCache || {};\n var cachedVal = cache[edgeWidth + ', ' + scale];\n\n if (cachedVal) {\n return cachedVal;\n }\n\n cachedVal = Math.max(Math.pow(edgeWidth * 13.37, 0.9), 29) * scale;\n cache[edgeWidth + ', ' + scale] = cachedVal;\n return cachedVal;\n};\n\nvar BRp$3 = {};\n\nBRp$3.findHaystackPoints = function (edges) {\n for (var i = 0; i < edges.length; i++) {\n var edge = edges[i];\n var _p = edge._private;\n var rs = _p.rscratch;\n\n if (!rs.haystack) {\n var angle = Math.random() * 2 * Math.PI;\n rs.source = {\n x: Math.cos(angle),\n y: Math.sin(angle)\n };\n angle = Math.random() * 2 * Math.PI;\n rs.target = {\n x: Math.cos(angle),\n y: Math.sin(angle)\n };\n }\n\n var src = _p.source;\n var tgt = _p.target;\n var srcPos = src.position();\n var tgtPos = tgt.position();\n var srcW = src.width();\n var tgtW = tgt.width();\n var srcH = src.height();\n var tgtH = tgt.height();\n var radius = edge.pstyle('haystack-radius').value;\n var halfRadius = radius / 2; // b/c have to half width/height\n\n rs.haystackPts = rs.allpts = [rs.source.x * srcW * halfRadius + srcPos.x, rs.source.y * srcH * halfRadius + srcPos.y, rs.target.x * tgtW * halfRadius + tgtPos.x, rs.target.y * tgtH * halfRadius + tgtPos.y];\n rs.midX = (rs.allpts[0] + rs.allpts[2]) / 2;\n rs.midY = (rs.allpts[1] + rs.allpts[3]) / 2; // always override as haystack in case set to different type previously\n\n rs.edgeType = 'haystack';\n rs.haystack = true;\n this.storeEdgeProjections(edge);\n this.calculateArrowAngles(edge);\n this.recalculateEdgeLabelProjections(edge);\n this.calculateLabelAngles(edge);\n }\n};\n\nBRp$3.findSegmentsPoints = function (edge, pairInfo) {\n // Segments (multiple straight lines)\n var rs = edge._private.rscratch;\n var posPts = pairInfo.posPts,\n intersectionPts = pairInfo.intersectionPts,\n vectorNormInverse = pairInfo.vectorNormInverse;\n var edgeDistances = edge.pstyle('edge-distances').value;\n var segmentWs = edge.pstyle('segment-weights');\n var segmentDs = edge.pstyle('segment-distances');\n var segmentsN = Math.min(segmentWs.pfValue.length, segmentDs.pfValue.length);\n rs.edgeType = 'segments';\n rs.segpts = [];\n\n for (var s = 0; s < segmentsN; s++) {\n var w = segmentWs.pfValue[s];\n var d = segmentDs.pfValue[s];\n var w1 = 1 - w;\n var w2 = w;\n var midptPts = edgeDistances === 'node-position' ? posPts : intersectionPts;\n var adjustedMidpt = {\n x: midptPts.x1 * w1 + midptPts.x2 * w2,\n y: midptPts.y1 * w1 + midptPts.y2 * w2\n };\n rs.segpts.push(adjustedMidpt.x + vectorNormInverse.x * d, adjustedMidpt.y + vectorNormInverse.y * d);\n }\n};\n\nBRp$3.findLoopPoints = function (edge, pairInfo, i, edgeIsUnbundled) {\n // Self-edge\n var rs = edge._private.rscratch;\n var dirCounts = pairInfo.dirCounts,\n srcPos = pairInfo.srcPos;\n var ctrlptDists = edge.pstyle('control-point-distances');\n var ctrlptDist = ctrlptDists ? ctrlptDists.pfValue[0] : undefined;\n var loopDir = edge.pstyle('loop-direction').pfValue;\n var loopSwp = edge.pstyle('loop-sweep').pfValue;\n var stepSize = edge.pstyle('control-point-step-size').pfValue;\n rs.edgeType = 'self';\n var j = i;\n var loopDist = stepSize;\n\n if (edgeIsUnbundled) {\n j = 0;\n loopDist = ctrlptDist;\n }\n\n var loopAngle = loopDir - Math.PI / 2;\n var outAngle = loopAngle - loopSwp / 2;\n var inAngle = loopAngle + loopSwp / 2; // increase by step size for overlapping loops, keyed on direction and sweep values\n\n var dc = String(loopDir + '_' + loopSwp);\n j = dirCounts[dc] === undefined ? dirCounts[dc] = 0 : ++dirCounts[dc];\n rs.ctrlpts = [srcPos.x + Math.cos(outAngle) * 1.4 * loopDist * (j / 3 + 1), srcPos.y + Math.sin(outAngle) * 1.4 * loopDist * (j / 3 + 1), srcPos.x + Math.cos(inAngle) * 1.4 * loopDist * (j / 3 + 1), srcPos.y + Math.sin(inAngle) * 1.4 * loopDist * (j / 3 + 1)];\n};\n\nBRp$3.findCompoundLoopPoints = function (edge, pairInfo, i, edgeIsUnbundled) {\n // Compound edge\n var rs = edge._private.rscratch;\n rs.edgeType = 'compound';\n var srcPos = pairInfo.srcPos,\n tgtPos = pairInfo.tgtPos,\n srcW = pairInfo.srcW,\n srcH = pairInfo.srcH,\n tgtW = pairInfo.tgtW,\n tgtH = pairInfo.tgtH;\n var stepSize = edge.pstyle('control-point-step-size').pfValue;\n var ctrlptDists = edge.pstyle('control-point-distances');\n var ctrlptDist = ctrlptDists ? ctrlptDists.pfValue[0] : undefined;\n var j = i;\n var loopDist = stepSize;\n\n if (edgeIsUnbundled) {\n j = 0;\n loopDist = ctrlptDist;\n }\n\n var loopW = 50;\n var loopaPos = {\n x: srcPos.x - srcW / 2,\n y: srcPos.y - srcH / 2\n };\n var loopbPos = {\n x: tgtPos.x - tgtW / 2,\n y: tgtPos.y - tgtH / 2\n };\n var loopPos = {\n x: Math.min(loopaPos.x, loopbPos.x),\n y: Math.min(loopaPos.y, loopbPos.y)\n }; // avoids cases with impossible beziers\n\n var minCompoundStretch = 0.5;\n var compoundStretchA = Math.max(minCompoundStretch, Math.log(srcW * 0.01));\n var compoundStretchB = Math.max(minCompoundStretch, Math.log(tgtW * 0.01));\n rs.ctrlpts = [loopPos.x, loopPos.y - (1 + Math.pow(loopW, 1.12) / 100) * loopDist * (j / 3 + 1) * compoundStretchA, loopPos.x - (1 + Math.pow(loopW, 1.12) / 100) * loopDist * (j / 3 + 1) * compoundStretchB, loopPos.y];\n};\n\nBRp$3.findStraightEdgePoints = function (edge) {\n // Straight edge within bundle\n edge._private.rscratch.edgeType = 'straight';\n};\n\nBRp$3.findBezierPoints = function (edge, pairInfo, i, edgeIsUnbundled, edgeIsSwapped) {\n var rs = edge._private.rscratch;\n var vectorNormInverse = pairInfo.vectorNormInverse,\n posPts = pairInfo.posPts,\n intersectionPts = pairInfo.intersectionPts;\n var edgeDistances = edge.pstyle('edge-distances').value;\n var stepSize = edge.pstyle('control-point-step-size').pfValue;\n var ctrlptDists = edge.pstyle('control-point-distances');\n var ctrlptWs = edge.pstyle('control-point-weights');\n var bezierN = ctrlptDists && ctrlptWs ? Math.min(ctrlptDists.value.length, ctrlptWs.value.length) : 1;\n var ctrlptDist = ctrlptDists ? ctrlptDists.pfValue[0] : undefined;\n var ctrlptWeight = ctrlptWs.value[0]; // (Multi)bezier\n\n var multi = edgeIsUnbundled;\n rs.edgeType = multi ? 'multibezier' : 'bezier';\n rs.ctrlpts = [];\n\n for (var b = 0; b < bezierN; b++) {\n var normctrlptDist = (0.5 - pairInfo.eles.length / 2 + i) * stepSize * (edgeIsSwapped ? -1 : 1);\n var manctrlptDist = void 0;\n var sign = signum(normctrlptDist);\n\n if (multi) {\n ctrlptDist = ctrlptDists ? ctrlptDists.pfValue[b] : stepSize; // fall back on step size\n\n ctrlptWeight = ctrlptWs.value[b];\n }\n\n if (edgeIsUnbundled) {\n // multi or single unbundled\n manctrlptDist = ctrlptDist;\n } else {\n manctrlptDist = ctrlptDist !== undefined ? sign * ctrlptDist : undefined;\n }\n\n var distanceFromMidpoint = manctrlptDist !== undefined ? manctrlptDist : normctrlptDist;\n var w1 = 1 - ctrlptWeight;\n var w2 = ctrlptWeight;\n var midptPts = edgeDistances === 'node-position' ? posPts : intersectionPts;\n var adjustedMidpt = {\n x: midptPts.x1 * w1 + midptPts.x2 * w2,\n y: midptPts.y1 * w1 + midptPts.y2 * w2\n };\n rs.ctrlpts.push(adjustedMidpt.x + vectorNormInverse.x * distanceFromMidpoint, adjustedMidpt.y + vectorNormInverse.y * distanceFromMidpoint);\n }\n};\n\nBRp$3.findTaxiPoints = function (edge, pairInfo) {\n // Taxicab geometry with two turns maximum\n var rs = edge._private.rscratch;\n rs.edgeType = 'segments';\n var VERTICAL = 'vertical';\n var HORIZONTAL = 'horizontal';\n var LEFTWARD = 'leftward';\n var RIGHTWARD = 'rightward';\n var DOWNWARD = 'downward';\n var UPWARD = 'upward';\n var AUTO = 'auto';\n var posPts = pairInfo.posPts,\n srcW = pairInfo.srcW,\n srcH = pairInfo.srcH,\n tgtW = pairInfo.tgtW,\n tgtH = pairInfo.tgtH;\n var edgeDistances = edge.pstyle('edge-distances').value;\n var dIncludesNodeBody = edgeDistances !== 'node-position';\n var taxiDir = edge.pstyle('taxi-direction').value;\n var rawTaxiDir = taxiDir; // unprocessed value\n\n var taxiTurn = edge.pstyle('taxi-turn');\n var turnIsPercent = taxiTurn.units === '%';\n var taxiTurnPfVal = taxiTurn.pfValue;\n var turnIsNegative = taxiTurnPfVal < 0; // i.e. from target side\n\n var minD = edge.pstyle('taxi-turn-min-distance').pfValue;\n var dw = dIncludesNodeBody ? (srcW + tgtW) / 2 : 0;\n var dh = dIncludesNodeBody ? (srcH + tgtH) / 2 : 0;\n var pdx = posPts.x2 - posPts.x1;\n var pdy = posPts.y2 - posPts.y1; // take away the effective w/h from the magnitude of the delta value\n\n var subDWH = function subDWH(dxy, dwh) {\n if (dxy > 0) {\n return Math.max(dxy - dwh, 0);\n } else {\n return Math.min(dxy + dwh, 0);\n }\n };\n\n var dx = subDWH(pdx, dw);\n var dy = subDWH(pdy, dh);\n var isExplicitDir = false;\n\n if (rawTaxiDir === AUTO) {\n taxiDir = Math.abs(dx) > Math.abs(dy) ? HORIZONTAL : VERTICAL;\n } else if (rawTaxiDir === UPWARD || rawTaxiDir === DOWNWARD) {\n taxiDir = VERTICAL;\n isExplicitDir = true;\n } else if (rawTaxiDir === LEFTWARD || rawTaxiDir === RIGHTWARD) {\n taxiDir = HORIZONTAL;\n isExplicitDir = true;\n }\n\n var isVert = taxiDir === VERTICAL;\n var l = isVert ? dy : dx;\n var pl = isVert ? pdy : pdx;\n var sgnL = signum(pl);\n var forcedDir = false;\n\n if (!(isExplicitDir && (turnIsPercent || turnIsNegative)) // forcing in this case would cause weird growing in the opposite direction\n && (rawTaxiDir === DOWNWARD && pl < 0 || rawTaxiDir === UPWARD && pl > 0 || rawTaxiDir === LEFTWARD && pl > 0 || rawTaxiDir === RIGHTWARD && pl < 0)) {\n sgnL *= -1;\n l = sgnL * Math.abs(l);\n forcedDir = true;\n }\n\n var d;\n\n if (turnIsPercent) {\n var p = taxiTurnPfVal < 0 ? 1 + taxiTurnPfVal : taxiTurnPfVal;\n d = p * l;\n } else {\n var k = taxiTurnPfVal < 0 ? l : 0;\n d = k + taxiTurnPfVal * sgnL;\n }\n\n var getIsTooClose = function getIsTooClose(d) {\n return Math.abs(d) < minD || Math.abs(d) >= Math.abs(l);\n };\n\n var isTooCloseSrc = getIsTooClose(d);\n var isTooCloseTgt = getIsTooClose(Math.abs(l) - Math.abs(d));\n var isTooClose = isTooCloseSrc || isTooCloseTgt;\n\n if (isTooClose && !forcedDir) {\n // non-ideal routing\n if (isVert) {\n // vertical fallbacks\n var lShapeInsideSrc = Math.abs(pl) <= srcH / 2;\n var lShapeInsideTgt = Math.abs(pdx) <= tgtW / 2;\n\n if (lShapeInsideSrc) {\n // horizontal Z-shape (direction not respected)\n var x = (posPts.x1 + posPts.x2) / 2;\n var y1 = posPts.y1,\n y2 = posPts.y2;\n rs.segpts = [x, y1, x, y2];\n } else if (lShapeInsideTgt) {\n // vertical Z-shape (distance not respected)\n var y = (posPts.y1 + posPts.y2) / 2;\n var x1 = posPts.x1,\n x2 = posPts.x2;\n rs.segpts = [x1, y, x2, y];\n } else {\n // L-shape fallback (turn distance not respected, but works well with tree siblings)\n rs.segpts = [posPts.x1, posPts.y2];\n }\n } else {\n // horizontal fallbacks\n var _lShapeInsideSrc = Math.abs(pl) <= srcW / 2;\n\n var _lShapeInsideTgt = Math.abs(pdy) <= tgtH / 2;\n\n if (_lShapeInsideSrc) {\n // vertical Z-shape (direction not respected)\n var _y = (posPts.y1 + posPts.y2) / 2;\n\n var _x = posPts.x1,\n _x2 = posPts.x2;\n rs.segpts = [_x, _y, _x2, _y];\n } else if (_lShapeInsideTgt) {\n // horizontal Z-shape (turn distance not respected)\n var _x3 = (posPts.x1 + posPts.x2) / 2;\n\n var _y2 = posPts.y1,\n _y3 = posPts.y2;\n rs.segpts = [_x3, _y2, _x3, _y3];\n } else {\n // L-shape (turn distance not respected, but works well for tree siblings)\n rs.segpts = [posPts.x2, posPts.y1];\n }\n }\n } else {\n // ideal routing\n if (isVert) {\n var _y4 = posPts.y1 + d + (dIncludesNodeBody ? srcH / 2 * sgnL : 0);\n\n var _x4 = posPts.x1,\n _x5 = posPts.x2;\n rs.segpts = [_x4, _y4, _x5, _y4];\n } else {\n // horizontal\n var _x6 = posPts.x1 + d + (dIncludesNodeBody ? srcW / 2 * sgnL : 0);\n\n var _y5 = posPts.y1,\n _y6 = posPts.y2;\n rs.segpts = [_x6, _y5, _x6, _y6];\n }\n }\n};\n\nBRp$3.tryToCorrectInvalidPoints = function (edge, pairInfo) {\n var rs = edge._private.rscratch; // can only correct beziers for now...\n\n if (rs.edgeType === 'bezier') {\n var srcPos = pairInfo.srcPos,\n tgtPos = pairInfo.tgtPos,\n srcW = pairInfo.srcW,\n srcH = pairInfo.srcH,\n tgtW = pairInfo.tgtW,\n tgtH = pairInfo.tgtH,\n srcShape = pairInfo.srcShape,\n tgtShape = pairInfo.tgtShape;\n var badStart = !number(rs.startX) || !number(rs.startY);\n var badAStart = !number(rs.arrowStartX) || !number(rs.arrowStartY);\n var badEnd = !number(rs.endX) || !number(rs.endY);\n var badAEnd = !number(rs.arrowEndX) || !number(rs.arrowEndY);\n var minCpADistFactor = 3;\n var arrowW = this.getArrowWidth(edge.pstyle('width').pfValue, edge.pstyle('arrow-scale').value) * this.arrowShapeWidth;\n var minCpADist = minCpADistFactor * arrowW;\n var startACpDist = dist({\n x: rs.ctrlpts[0],\n y: rs.ctrlpts[1]\n }, {\n x: rs.startX,\n y: rs.startY\n });\n var closeStartACp = startACpDist < minCpADist;\n var endACpDist = dist({\n x: rs.ctrlpts[0],\n y: rs.ctrlpts[1]\n }, {\n x: rs.endX,\n y: rs.endY\n });\n var closeEndACp = endACpDist < minCpADist;\n var overlapping = false;\n\n if (badStart || badAStart || closeStartACp) {\n overlapping = true; // project control point along line from src centre to outside the src shape\n // (otherwise intersection will yield nothing)\n\n var cpD = {\n // delta\n x: rs.ctrlpts[0] - srcPos.x,\n y: rs.ctrlpts[1] - srcPos.y\n };\n var cpL = Math.sqrt(cpD.x * cpD.x + cpD.y * cpD.y); // length of line\n\n var cpM = {\n // normalised delta\n x: cpD.x / cpL,\n y: cpD.y / cpL\n };\n var radius = Math.max(srcW, srcH);\n var cpProj = {\n // *2 radius guarantees outside shape\n x: rs.ctrlpts[0] + cpM.x * 2 * radius,\n y: rs.ctrlpts[1] + cpM.y * 2 * radius\n };\n var srcCtrlPtIntn = srcShape.intersectLine(srcPos.x, srcPos.y, srcW, srcH, cpProj.x, cpProj.y, 0);\n\n if (closeStartACp) {\n rs.ctrlpts[0] = rs.ctrlpts[0] + cpM.x * (minCpADist - startACpDist);\n rs.ctrlpts[1] = rs.ctrlpts[1] + cpM.y * (minCpADist - startACpDist);\n } else {\n rs.ctrlpts[0] = srcCtrlPtIntn[0] + cpM.x * minCpADist;\n rs.ctrlpts[1] = srcCtrlPtIntn[1] + cpM.y * minCpADist;\n }\n }\n\n if (badEnd || badAEnd || closeEndACp) {\n overlapping = true; // project control point along line from tgt centre to outside the tgt shape\n // (otherwise intersection will yield nothing)\n\n var _cpD = {\n // delta\n x: rs.ctrlpts[0] - tgtPos.x,\n y: rs.ctrlpts[1] - tgtPos.y\n };\n\n var _cpL = Math.sqrt(_cpD.x * _cpD.x + _cpD.y * _cpD.y); // length of line\n\n\n var _cpM = {\n // normalised delta\n x: _cpD.x / _cpL,\n y: _cpD.y / _cpL\n };\n\n var _radius = Math.max(srcW, srcH);\n\n var _cpProj = {\n // *2 radius guarantees outside shape\n x: rs.ctrlpts[0] + _cpM.x * 2 * _radius,\n y: rs.ctrlpts[1] + _cpM.y * 2 * _radius\n };\n var tgtCtrlPtIntn = tgtShape.intersectLine(tgtPos.x, tgtPos.y, tgtW, tgtH, _cpProj.x, _cpProj.y, 0);\n\n if (closeEndACp) {\n rs.ctrlpts[0] = rs.ctrlpts[0] + _cpM.x * (minCpADist - endACpDist);\n rs.ctrlpts[1] = rs.ctrlpts[1] + _cpM.y * (minCpADist - endACpDist);\n } else {\n rs.ctrlpts[0] = tgtCtrlPtIntn[0] + _cpM.x * minCpADist;\n rs.ctrlpts[1] = tgtCtrlPtIntn[1] + _cpM.y * minCpADist;\n }\n }\n\n if (overlapping) {\n // recalc endpts\n this.findEndpoints(edge);\n }\n }\n};\n\nBRp$3.storeAllpts = function (edge) {\n var rs = edge._private.rscratch;\n\n if (rs.edgeType === 'multibezier' || rs.edgeType === 'bezier' || rs.edgeType === 'self' || rs.edgeType === 'compound') {\n rs.allpts = [];\n rs.allpts.push(rs.startX, rs.startY);\n\n for (var b = 0; b + 1 < rs.ctrlpts.length; b += 2) {\n // ctrl pt itself\n rs.allpts.push(rs.ctrlpts[b], rs.ctrlpts[b + 1]); // the midpt between ctrlpts as intermediate destination pts\n\n if (b + 3 < rs.ctrlpts.length) {\n rs.allpts.push((rs.ctrlpts[b] + rs.ctrlpts[b + 2]) / 2, (rs.ctrlpts[b + 1] + rs.ctrlpts[b + 3]) / 2);\n }\n }\n\n rs.allpts.push(rs.endX, rs.endY);\n var m, mt;\n\n if (rs.ctrlpts.length / 2 % 2 === 0) {\n m = rs.allpts.length / 2 - 1;\n rs.midX = rs.allpts[m];\n rs.midY = rs.allpts[m + 1];\n } else {\n m = rs.allpts.length / 2 - 3;\n mt = 0.5;\n rs.midX = qbezierAt(rs.allpts[m], rs.allpts[m + 2], rs.allpts[m + 4], mt);\n rs.midY = qbezierAt(rs.allpts[m + 1], rs.allpts[m + 3], rs.allpts[m + 5], mt);\n }\n } else if (rs.edgeType === 'straight') {\n // need to calc these after endpts\n rs.allpts = [rs.startX, rs.startY, rs.endX, rs.endY]; // default midpt for labels etc\n\n rs.midX = (rs.startX + rs.endX + rs.arrowStartX + rs.arrowEndX) / 4;\n rs.midY = (rs.startY + rs.endY + rs.arrowStartY + rs.arrowEndY) / 4;\n } else if (rs.edgeType === 'segments') {\n rs.allpts = [];\n rs.allpts.push(rs.startX, rs.startY);\n rs.allpts.push.apply(rs.allpts, rs.segpts);\n rs.allpts.push(rs.endX, rs.endY);\n\n if (rs.segpts.length % 4 === 0) {\n var i2 = rs.segpts.length / 2;\n var i1 = i2 - 2;\n rs.midX = (rs.segpts[i1] + rs.segpts[i2]) / 2;\n rs.midY = (rs.segpts[i1 + 1] + rs.segpts[i2 + 1]) / 2;\n } else {\n var _i = rs.segpts.length / 2 - 1;\n\n rs.midX = rs.segpts[_i];\n rs.midY = rs.segpts[_i + 1];\n }\n }\n};\n\nBRp$3.checkForInvalidEdgeWarning = function (edge) {\n var rs = edge[0]._private.rscratch;\n\n if (rs.nodesOverlap || number(rs.startX) && number(rs.startY) && number(rs.endX) && number(rs.endY)) {\n rs.loggedErr = false;\n } else {\n if (!rs.loggedErr) {\n rs.loggedErr = true;\n warn('Edge `' + edge.id() + '` has invalid endpoints and so it is impossible to draw. Adjust your edge style (e.g. control points) accordingly or use an alternative edge type. This is expected behaviour when the source node and the target node overlap.');\n }\n }\n};\n\nBRp$3.findEdgeControlPoints = function (edges) {\n var _this = this;\n\n if (!edges || edges.length === 0) {\n return;\n }\n\n var r = this;\n var cy = r.cy;\n var hasCompounds = cy.hasCompoundNodes();\n var hashTable = {\n map: new Map$1(),\n get: function get(pairId) {\n var map2 = this.map.get(pairId[0]);\n\n if (map2 != null) {\n return map2.get(pairId[1]);\n } else {\n return null;\n }\n },\n set: function set(pairId, val) {\n var map2 = this.map.get(pairId[0]);\n\n if (map2 == null) {\n map2 = new Map$1();\n this.map.set(pairId[0], map2);\n }\n\n map2.set(pairId[1], val);\n }\n };\n var pairIds = [];\n var haystackEdges = []; // create a table of edge (src, tgt) => list of edges between them\n\n for (var i = 0; i < edges.length; i++) {\n var edge = edges[i];\n var _p = edge._private;\n var curveStyle = edge.pstyle('curve-style').value; // ignore edges who are not to be displayed\n // they shouldn't take up space\n\n if (edge.removed() || !edge.takesUpSpace()) {\n continue;\n }\n\n if (curveStyle === 'haystack') {\n haystackEdges.push(edge);\n continue;\n }\n\n var edgeIsUnbundled = curveStyle === 'unbundled-bezier' || curveStyle === 'segments' || curveStyle === 'straight' || curveStyle === 'taxi';\n var edgeIsBezier = curveStyle === 'unbundled-bezier' || curveStyle === 'bezier';\n var src = _p.source;\n var tgt = _p.target;\n var srcIndex = src.poolIndex();\n var tgtIndex = tgt.poolIndex();\n var pairId = [srcIndex, tgtIndex].sort();\n var tableEntry = hashTable.get(pairId);\n\n if (tableEntry == null) {\n tableEntry = {\n eles: []\n };\n hashTable.set(pairId, tableEntry);\n pairIds.push(pairId);\n }\n\n tableEntry.eles.push(edge);\n\n if (edgeIsUnbundled) {\n tableEntry.hasUnbundled = true;\n }\n\n if (edgeIsBezier) {\n tableEntry.hasBezier = true;\n }\n } // for each pair (src, tgt), create the ctrl pts\n // Nested for loop is OK; total number of iterations for both loops = edgeCount\n\n\n var _loop = function _loop(p) {\n var pairId = pairIds[p];\n var pairInfo = hashTable.get(pairId);\n var swappedpairInfo = void 0;\n\n if (!pairInfo.hasUnbundled) {\n var pllEdges = pairInfo.eles[0].parallelEdges().filter(function (e) {\n return e.isBundledBezier();\n });\n clearArray(pairInfo.eles);\n pllEdges.forEach(function (edge) {\n return pairInfo.eles.push(edge);\n }); // for each pair id, the edges should be sorted by index\n\n pairInfo.eles.sort(function (edge1, edge2) {\n return edge1.poolIndex() - edge2.poolIndex();\n });\n }\n\n var firstEdge = pairInfo.eles[0];\n var src = firstEdge.source();\n var tgt = firstEdge.target(); // make sure src/tgt distinction is consistent w.r.t. pairId\n\n if (src.poolIndex() > tgt.poolIndex()) {\n var temp = src;\n src = tgt;\n tgt = temp;\n }\n\n var srcPos = pairInfo.srcPos = src.position();\n var tgtPos = pairInfo.tgtPos = tgt.position();\n var srcW = pairInfo.srcW = src.outerWidth();\n var srcH = pairInfo.srcH = src.outerHeight();\n var tgtW = pairInfo.tgtW = tgt.outerWidth();\n var tgtH = pairInfo.tgtH = tgt.outerHeight();\n\n var srcShape = pairInfo.srcShape = r.nodeShapes[_this.getNodeShape(src)];\n\n var tgtShape = pairInfo.tgtShape = r.nodeShapes[_this.getNodeShape(tgt)];\n\n pairInfo.dirCounts = {\n 'north': 0,\n 'west': 0,\n 'south': 0,\n 'east': 0,\n 'northwest': 0,\n 'southwest': 0,\n 'northeast': 0,\n 'southeast': 0\n };\n\n for (var _i2 = 0; _i2 < pairInfo.eles.length; _i2++) {\n var _edge = pairInfo.eles[_i2];\n var rs = _edge[0]._private.rscratch;\n\n var _curveStyle = _edge.pstyle('curve-style').value;\n\n var _edgeIsUnbundled = _curveStyle === 'unbundled-bezier' || _curveStyle === 'segments' || _curveStyle === 'taxi'; // whether the normalised pair order is the reverse of the edge's src-tgt order\n\n\n var edgeIsSwapped = !src.same(_edge.source());\n\n if (!pairInfo.calculatedIntersection && src !== tgt && (pairInfo.hasBezier || pairInfo.hasUnbundled)) {\n pairInfo.calculatedIntersection = true; // pt outside src shape to calc distance/displacement from src to tgt\n\n var srcOutside = srcShape.intersectLine(srcPos.x, srcPos.y, srcW, srcH, tgtPos.x, tgtPos.y, 0);\n var srcIntn = pairInfo.srcIntn = srcOutside; // pt outside tgt shape to calc distance/displacement from src to tgt\n\n var tgtOutside = tgtShape.intersectLine(tgtPos.x, tgtPos.y, tgtW, tgtH, srcPos.x, srcPos.y, 0);\n var tgtIntn = pairInfo.tgtIntn = tgtOutside;\n var intersectionPts = pairInfo.intersectionPts = {\n x1: srcOutside[0],\n x2: tgtOutside[0],\n y1: srcOutside[1],\n y2: tgtOutside[1]\n };\n var posPts = pairInfo.posPts = {\n x1: srcPos.x,\n x2: tgtPos.x,\n y1: srcPos.y,\n y2: tgtPos.y\n };\n var dy = tgtOutside[1] - srcOutside[1];\n var dx = tgtOutside[0] - srcOutside[0];\n var l = Math.sqrt(dx * dx + dy * dy);\n var vector = pairInfo.vector = {\n x: dx,\n y: dy\n };\n var vectorNorm = pairInfo.vectorNorm = {\n x: vector.x / l,\n y: vector.y / l\n };\n var vectorNormInverse = {\n x: -vectorNorm.y,\n y: vectorNorm.x\n }; // if node shapes overlap, then no ctrl pts to draw\n\n pairInfo.nodesOverlap = !number(l) || tgtShape.checkPoint(srcOutside[0], srcOutside[1], 0, tgtW, tgtH, tgtPos.x, tgtPos.y) || srcShape.checkPoint(tgtOutside[0], tgtOutside[1], 0, srcW, srcH, srcPos.x, srcPos.y);\n pairInfo.vectorNormInverse = vectorNormInverse;\n swappedpairInfo = {\n nodesOverlap: pairInfo.nodesOverlap,\n dirCounts: pairInfo.dirCounts,\n calculatedIntersection: true,\n hasBezier: pairInfo.hasBezier,\n hasUnbundled: pairInfo.hasUnbundled,\n eles: pairInfo.eles,\n srcPos: tgtPos,\n tgtPos: srcPos,\n srcW: tgtW,\n srcH: tgtH,\n tgtW: srcW,\n tgtH: srcH,\n srcIntn: tgtIntn,\n tgtIntn: srcIntn,\n srcShape: tgtShape,\n tgtShape: srcShape,\n posPts: {\n x1: posPts.x2,\n y1: posPts.y2,\n x2: posPts.x1,\n y2: posPts.y1\n },\n intersectionPts: {\n x1: intersectionPts.x2,\n y1: intersectionPts.y2,\n x2: intersectionPts.x1,\n y2: intersectionPts.y1\n },\n vector: {\n x: -vector.x,\n y: -vector.y\n },\n vectorNorm: {\n x: -vectorNorm.x,\n y: -vectorNorm.y\n },\n vectorNormInverse: {\n x: -vectorNormInverse.x,\n y: -vectorNormInverse.y\n }\n };\n }\n\n var passedPairInfo = edgeIsSwapped ? swappedpairInfo : pairInfo;\n rs.nodesOverlap = passedPairInfo.nodesOverlap;\n rs.srcIntn = passedPairInfo.srcIntn;\n rs.tgtIntn = passedPairInfo.tgtIntn;\n\n if (hasCompounds && (src.isParent() || src.isChild() || tgt.isParent() || tgt.isChild()) && (src.parents().anySame(tgt) || tgt.parents().anySame(src) || src.same(tgt) && src.isParent())) {\n _this.findCompoundLoopPoints(_edge, passedPairInfo, _i2, _edgeIsUnbundled);\n } else if (src === tgt) {\n _this.findLoopPoints(_edge, passedPairInfo, _i2, _edgeIsUnbundled);\n } else if (_curveStyle === 'segments') {\n _this.findSegmentsPoints(_edge, passedPairInfo);\n } else if (_curveStyle === 'taxi') {\n _this.findTaxiPoints(_edge, passedPairInfo);\n } else if (_curveStyle === 'straight' || !_edgeIsUnbundled && pairInfo.eles.length % 2 === 1 && _i2 === Math.floor(pairInfo.eles.length / 2)) {\n _this.findStraightEdgePoints(_edge);\n } else {\n _this.findBezierPoints(_edge, passedPairInfo, _i2, _edgeIsUnbundled, edgeIsSwapped);\n }\n\n _this.findEndpoints(_edge);\n\n _this.tryToCorrectInvalidPoints(_edge, passedPairInfo);\n\n _this.checkForInvalidEdgeWarning(_edge);\n\n _this.storeAllpts(_edge);\n\n _this.storeEdgeProjections(_edge);\n\n _this.calculateArrowAngles(_edge);\n\n _this.recalculateEdgeLabelProjections(_edge);\n\n _this.calculateLabelAngles(_edge);\n } // for pair edges\n\n };\n\n for (var p = 0; p < pairIds.length; p++) {\n _loop(p);\n } // for pair ids\n // haystacks avoid the expense of pairInfo stuff (intersections etc.)\n\n\n this.findHaystackPoints(haystackEdges);\n};\n\nfunction getPts(pts) {\n var retPts = [];\n\n if (pts == null) {\n return;\n }\n\n for (var i = 0; i < pts.length; i += 2) {\n var x = pts[i];\n var y = pts[i + 1];\n retPts.push({\n x: x,\n y: y\n });\n }\n\n return retPts;\n}\n\nBRp$3.getSegmentPoints = function (edge) {\n var rs = edge[0]._private.rscratch;\n var type = rs.edgeType;\n\n if (type === 'segments') {\n this.recalculateRenderedStyle(edge);\n return getPts(rs.segpts);\n }\n};\n\nBRp$3.getControlPoints = function (edge) {\n var rs = edge[0]._private.rscratch;\n var type = rs.edgeType;\n\n if (type === 'bezier' || type === 'multibezier' || type === 'self' || type === 'compound') {\n this.recalculateRenderedStyle(edge);\n return getPts(rs.ctrlpts);\n }\n};\n\nBRp$3.getEdgeMidpoint = function (edge) {\n var rs = edge[0]._private.rscratch;\n this.recalculateRenderedStyle(edge);\n return {\n x: rs.midX,\n y: rs.midY\n };\n};\n\nvar BRp$4 = {};\n\nBRp$4.manualEndptToPx = function (node, prop) {\n var r = this;\n var npos = node.position();\n var w = node.outerWidth();\n var h = node.outerHeight();\n\n if (prop.value.length === 2) {\n var p = [prop.pfValue[0], prop.pfValue[1]];\n\n if (prop.units[0] === '%') {\n p[0] = p[0] * w;\n }\n\n if (prop.units[1] === '%') {\n p[1] = p[1] * h;\n }\n\n p[0] += npos.x;\n p[1] += npos.y;\n return p;\n } else {\n var angle = prop.pfValue[0];\n angle = -Math.PI / 2 + angle; // start at 12 o'clock\n\n var l = 2 * Math.max(w, h);\n var _p = [npos.x + Math.cos(angle) * l, npos.y + Math.sin(angle) * l];\n return r.nodeShapes[this.getNodeShape(node)].intersectLine(npos.x, npos.y, w, h, _p[0], _p[1], 0);\n }\n};\n\nBRp$4.findEndpoints = function (edge) {\n var r = this;\n var intersect;\n var source = edge.source()[0];\n var target = edge.target()[0];\n var srcPos = source.position();\n var tgtPos = target.position();\n var tgtArShape = edge.pstyle('target-arrow-shape').value;\n var srcArShape = edge.pstyle('source-arrow-shape').value;\n var tgtDist = edge.pstyle('target-distance-from-node').pfValue;\n var srcDist = edge.pstyle('source-distance-from-node').pfValue;\n var curveStyle = edge.pstyle('curve-style').value;\n var rs = edge._private.rscratch;\n var et = rs.edgeType;\n var taxi = curveStyle === 'taxi';\n var self = et === 'self' || et === 'compound';\n var bezier = et === 'bezier' || et === 'multibezier' || self;\n var multi = et !== 'bezier';\n var lines = et === 'straight' || et === 'segments';\n var segments = et === 'segments';\n var hasEndpts = bezier || multi || lines;\n var overrideEndpts = self || taxi;\n var srcManEndpt = edge.pstyle('source-endpoint');\n var srcManEndptVal = overrideEndpts ? 'outside-to-node' : srcManEndpt.value;\n var tgtManEndpt = edge.pstyle('target-endpoint');\n var tgtManEndptVal = overrideEndpts ? 'outside-to-node' : tgtManEndpt.value;\n rs.srcManEndpt = srcManEndpt;\n rs.tgtManEndpt = tgtManEndpt;\n var p1; // last known point of edge on target side\n\n var p2; // last known point of edge on source side\n\n var p1_i; // point to intersect with target shape\n\n var p2_i; // point to intersect with source shape\n\n if (bezier) {\n var cpStart = [rs.ctrlpts[0], rs.ctrlpts[1]];\n var cpEnd = multi ? [rs.ctrlpts[rs.ctrlpts.length - 2], rs.ctrlpts[rs.ctrlpts.length - 1]] : cpStart;\n p1 = cpEnd;\n p2 = cpStart;\n } else if (lines) {\n var srcArrowFromPt = !segments ? [tgtPos.x, tgtPos.y] : rs.segpts.slice(0, 2);\n var tgtArrowFromPt = !segments ? [srcPos.x, srcPos.y] : rs.segpts.slice(rs.segpts.length - 2);\n p1 = tgtArrowFromPt;\n p2 = srcArrowFromPt;\n }\n\n if (tgtManEndptVal === 'inside-to-node') {\n intersect = [tgtPos.x, tgtPos.y];\n } else if (tgtManEndpt.units) {\n intersect = this.manualEndptToPx(target, tgtManEndpt);\n } else if (tgtManEndptVal === 'outside-to-line') {\n intersect = rs.tgtIntn; // use cached value from ctrlpt calc\n } else {\n if (tgtManEndptVal === 'outside-to-node' || tgtManEndptVal === 'outside-to-node-or-label') {\n p1_i = p1;\n } else if (tgtManEndptVal === 'outside-to-line' || tgtManEndptVal === 'outside-to-line-or-label') {\n p1_i = [srcPos.x, srcPos.y];\n }\n\n intersect = r.nodeShapes[this.getNodeShape(target)].intersectLine(tgtPos.x, tgtPos.y, target.outerWidth(), target.outerHeight(), p1_i[0], p1_i[1], 0);\n\n if (tgtManEndptVal === 'outside-to-node-or-label' || tgtManEndptVal === 'outside-to-line-or-label') {\n var trs = target._private.rscratch;\n var lw = trs.labelWidth;\n var lh = trs.labelHeight;\n var lx = trs.labelX;\n var ly = trs.labelY;\n var lw2 = lw / 2;\n var lh2 = lh / 2;\n var va = target.pstyle('text-valign').value;\n\n if (va === 'top') {\n ly -= lh2;\n } else if (va === 'bottom') {\n ly += lh2;\n }\n\n var ha = target.pstyle('text-halign').value;\n\n if (ha === 'left') {\n lx -= lw2;\n } else if (ha === 'right') {\n lx += lw2;\n }\n\n var labelIntersect = polygonIntersectLine(p1_i[0], p1_i[1], [lx - lw2, ly - lh2, lx + lw2, ly - lh2, lx + lw2, ly + lh2, lx - lw2, ly + lh2], tgtPos.x, tgtPos.y);\n\n if (labelIntersect.length > 0) {\n var refPt = srcPos;\n var intSqdist = sqdist(refPt, array2point(intersect));\n var labIntSqdist = sqdist(refPt, array2point(labelIntersect));\n var minSqDist = intSqdist;\n\n if (labIntSqdist < intSqdist) {\n intersect = labelIntersect;\n minSqDist = labIntSqdist;\n }\n\n if (labelIntersect.length > 2) {\n var labInt2SqDist = sqdist(refPt, {\n x: labelIntersect[2],\n y: labelIntersect[3]\n });\n\n if (labInt2SqDist < minSqDist) {\n intersect = [labelIntersect[2], labelIntersect[3]];\n }\n }\n }\n }\n }\n\n var arrowEnd = shortenIntersection(intersect, p1, r.arrowShapes[tgtArShape].spacing(edge) + tgtDist);\n var edgeEnd = shortenIntersection(intersect, p1, r.arrowShapes[tgtArShape].gap(edge) + tgtDist);\n rs.endX = edgeEnd[0];\n rs.endY = edgeEnd[1];\n rs.arrowEndX = arrowEnd[0];\n rs.arrowEndY = arrowEnd[1];\n\n if (srcManEndptVal === 'inside-to-node') {\n intersect = [srcPos.x, srcPos.y];\n } else if (srcManEndpt.units) {\n intersect = this.manualEndptToPx(source, srcManEndpt);\n } else if (srcManEndptVal === 'outside-to-line') {\n intersect = rs.srcIntn; // use cached value from ctrlpt calc\n } else {\n if (srcManEndptVal === 'outside-to-node' || srcManEndptVal === 'outside-to-node-or-label') {\n p2_i = p2;\n } else if (srcManEndptVal === 'outside-to-line' || srcManEndptVal === 'outside-to-line-or-label') {\n p2_i = [tgtPos.x, tgtPos.y];\n }\n\n intersect = r.nodeShapes[this.getNodeShape(source)].intersectLine(srcPos.x, srcPos.y, source.outerWidth(), source.outerHeight(), p2_i[0], p2_i[1], 0);\n\n if (srcManEndptVal === 'outside-to-node-or-label' || srcManEndptVal === 'outside-to-line-or-label') {\n var srs = source._private.rscratch;\n var _lw = srs.labelWidth;\n var _lh = srs.labelHeight;\n var _lx = srs.labelX;\n var _ly = srs.labelY;\n\n var _lw2 = _lw / 2;\n\n var _lh2 = _lh / 2;\n\n var _va = source.pstyle('text-valign').value;\n\n if (_va === 'top') {\n _ly -= _lh2;\n } else if (_va === 'bottom') {\n _ly += _lh2;\n }\n\n var _ha = source.pstyle('text-halign').value;\n\n if (_ha === 'left') {\n _lx -= _lw2;\n } else if (_ha === 'right') {\n _lx += _lw2;\n }\n\n var _labelIntersect = polygonIntersectLine(p2_i[0], p2_i[1], [_lx - _lw2, _ly - _lh2, _lx + _lw2, _ly - _lh2, _lx + _lw2, _ly + _lh2, _lx - _lw2, _ly + _lh2], srcPos.x, srcPos.y);\n\n if (_labelIntersect.length > 0) {\n var _refPt = tgtPos;\n\n var _intSqdist = sqdist(_refPt, array2point(intersect));\n\n var _labIntSqdist = sqdist(_refPt, array2point(_labelIntersect));\n\n var _minSqDist = _intSqdist;\n\n if (_labIntSqdist < _intSqdist) {\n intersect = [_labelIntersect[0], _labelIntersect[1]];\n _minSqDist = _labIntSqdist;\n }\n\n if (_labelIntersect.length > 2) {\n var _labInt2SqDist = sqdist(_refPt, {\n x: _labelIntersect[2],\n y: _labelIntersect[3]\n });\n\n if (_labInt2SqDist < _minSqDist) {\n intersect = [_labelIntersect[2], _labelIntersect[3]];\n }\n }\n }\n }\n }\n\n var arrowStart = shortenIntersection(intersect, p2, r.arrowShapes[srcArShape].spacing(edge) + srcDist);\n var edgeStart = shortenIntersection(intersect, p2, r.arrowShapes[srcArShape].gap(edge) + srcDist);\n rs.startX = edgeStart[0];\n rs.startY = edgeStart[1];\n rs.arrowStartX = arrowStart[0];\n rs.arrowStartY = arrowStart[1];\n\n if (hasEndpts) {\n if (!number(rs.startX) || !number(rs.startY) || !number(rs.endX) || !number(rs.endY)) {\n rs.badLine = true;\n } else {\n rs.badLine = false;\n }\n }\n};\n\nBRp$4.getSourceEndpoint = function (edge) {\n var rs = edge[0]._private.rscratch;\n this.recalculateRenderedStyle(edge);\n\n switch (rs.edgeType) {\n case 'haystack':\n return {\n x: rs.haystackPts[0],\n y: rs.haystackPts[1]\n };\n\n default:\n return {\n x: rs.arrowStartX,\n y: rs.arrowStartY\n };\n }\n};\n\nBRp$4.getTargetEndpoint = function (edge) {\n var rs = edge[0]._private.rscratch;\n this.recalculateRenderedStyle(edge);\n\n switch (rs.edgeType) {\n case 'haystack':\n return {\n x: rs.haystackPts[2],\n y: rs.haystackPts[3]\n };\n\n default:\n return {\n x: rs.arrowEndX,\n y: rs.arrowEndY\n };\n }\n};\n\nvar BRp$5 = {};\n\nfunction pushBezierPts(r, edge, pts) {\n var qbezierAt$1 = function qbezierAt$1(p1, p2, p3, t) {\n return qbezierAt(p1, p2, p3, t);\n };\n\n var _p = edge._private;\n var bpts = _p.rstyle.bezierPts;\n\n for (var i = 0; i < r.bezierProjPcts.length; i++) {\n var p = r.bezierProjPcts[i];\n bpts.push({\n x: qbezierAt$1(pts[0], pts[2], pts[4], p),\n y: qbezierAt$1(pts[1], pts[3], pts[5], p)\n });\n }\n}\n\nBRp$5.storeEdgeProjections = function (edge) {\n var _p = edge._private;\n var rs = _p.rscratch;\n var et = rs.edgeType; // clear the cached points state\n\n _p.rstyle.bezierPts = null;\n _p.rstyle.linePts = null;\n _p.rstyle.haystackPts = null;\n\n if (et === 'multibezier' || et === 'bezier' || et === 'self' || et === 'compound') {\n _p.rstyle.bezierPts = [];\n\n for (var i = 0; i + 5 < rs.allpts.length; i += 4) {\n pushBezierPts(this, edge, rs.allpts.slice(i, i + 6));\n }\n } else if (et === 'segments') {\n var lpts = _p.rstyle.linePts = [];\n\n for (var i = 0; i + 1 < rs.allpts.length; i += 2) {\n lpts.push({\n x: rs.allpts[i],\n y: rs.allpts[i + 1]\n });\n }\n } else if (et === 'haystack') {\n var hpts = rs.haystackPts;\n _p.rstyle.haystackPts = [{\n x: hpts[0],\n y: hpts[1]\n }, {\n x: hpts[2],\n y: hpts[3]\n }];\n }\n\n _p.rstyle.arrowWidth = this.getArrowWidth(edge.pstyle('width').pfValue, edge.pstyle('arrow-scale').value) * this.arrowShapeWidth;\n};\n\nBRp$5.recalculateEdgeProjections = function (edges) {\n this.findEdgeControlPoints(edges);\n};\n\n/* global document */\n\nvar BRp$6 = {};\n\nBRp$6.recalculateNodeLabelProjection = function (node) {\n var content = node.pstyle('label').strValue;\n\n if (emptyString(content)) {\n return;\n }\n\n var textX, textY;\n var _p = node._private;\n var nodeWidth = node.width();\n var nodeHeight = node.height();\n var padding = node.padding();\n var nodePos = node.position();\n var textHalign = node.pstyle('text-halign').strValue;\n var textValign = node.pstyle('text-valign').strValue;\n var rs = _p.rscratch;\n var rstyle = _p.rstyle;\n\n switch (textHalign) {\n case 'left':\n textX = nodePos.x - nodeWidth / 2 - padding;\n break;\n\n case 'right':\n textX = nodePos.x + nodeWidth / 2 + padding;\n break;\n\n default:\n // e.g. center\n textX = nodePos.x;\n }\n\n switch (textValign) {\n case 'top':\n textY = nodePos.y - nodeHeight / 2 - padding;\n break;\n\n case 'bottom':\n textY = nodePos.y + nodeHeight / 2 + padding;\n break;\n\n default:\n // e.g. middle\n textY = nodePos.y;\n }\n\n rs.labelX = textX;\n rs.labelY = textY;\n rstyle.labelX = textX;\n rstyle.labelY = textY;\n this.calculateLabelAngles(node);\n this.applyLabelDimensions(node);\n};\n\nvar lineAngleFromDelta = function lineAngleFromDelta(dx, dy) {\n var angle = Math.atan(dy / dx);\n\n if (dx === 0 && angle < 0) {\n angle = angle * -1;\n }\n\n return angle;\n};\n\nvar lineAngle = function lineAngle(p0, p1) {\n var dx = p1.x - p0.x;\n var dy = p1.y - p0.y;\n return lineAngleFromDelta(dx, dy);\n};\n\nvar bezierAngle = function bezierAngle(p0, p1, p2, t) {\n var t0 = bound(0, t - 0.001, 1);\n var t1 = bound(0, t + 0.001, 1);\n var lp0 = qbezierPtAt(p0, p1, p2, t0);\n var lp1 = qbezierPtAt(p0, p1, p2, t1);\n return lineAngle(lp0, lp1);\n};\n\nBRp$6.recalculateEdgeLabelProjections = function (edge) {\n var p;\n var _p = edge._private;\n var rs = _p.rscratch;\n var r = this;\n var content = {\n mid: edge.pstyle('label').strValue,\n source: edge.pstyle('source-label').strValue,\n target: edge.pstyle('target-label').strValue\n };\n\n if (content.mid || content.source || content.target) ; else {\n return; // no labels => no calcs\n } // add center point to style so bounding box calculations can use it\n //\n\n\n p = {\n x: rs.midX,\n y: rs.midY\n };\n\n var setRs = function setRs(propName, prefix, value) {\n setPrefixedProperty(_p.rscratch, propName, prefix, value);\n setPrefixedProperty(_p.rstyle, propName, prefix, value);\n };\n\n setRs('labelX', null, p.x);\n setRs('labelY', null, p.y);\n var midAngle = lineAngleFromDelta(rs.midDispX, rs.midDispY);\n setRs('labelAutoAngle', null, midAngle);\n\n var createControlPointInfo = function createControlPointInfo() {\n if (createControlPointInfo.cache) {\n return createControlPointInfo.cache;\n } // use cache so only 1x per edge\n\n\n var ctrlpts = []; // store each ctrlpt info init\n\n for (var i = 0; i + 5 < rs.allpts.length; i += 4) {\n var p0 = {\n x: rs.allpts[i],\n y: rs.allpts[i + 1]\n };\n var p1 = {\n x: rs.allpts[i + 2],\n y: rs.allpts[i + 3]\n }; // ctrlpt\n\n var p2 = {\n x: rs.allpts[i + 4],\n y: rs.allpts[i + 5]\n };\n ctrlpts.push({\n p0: p0,\n p1: p1,\n p2: p2,\n startDist: 0,\n length: 0,\n segments: []\n });\n }\n\n var bpts = _p.rstyle.bezierPts;\n var nProjs = r.bezierProjPcts.length;\n\n function addSegment(cp, p0, p1, t0, t1) {\n var length = dist(p0, p1);\n var prevSegment = cp.segments[cp.segments.length - 1];\n var segment = {\n p0: p0,\n p1: p1,\n t0: t0,\n t1: t1,\n startDist: prevSegment ? prevSegment.startDist + prevSegment.length : 0,\n length: length\n };\n cp.segments.push(segment);\n cp.length += length;\n } // update each ctrlpt with segment info\n\n\n for (var _i = 0; _i < ctrlpts.length; _i++) {\n var cp = ctrlpts[_i];\n var prevCp = ctrlpts[_i - 1];\n\n if (prevCp) {\n cp.startDist = prevCp.startDist + prevCp.length;\n }\n\n addSegment(cp, cp.p0, bpts[_i * nProjs], 0, r.bezierProjPcts[0]); // first\n\n for (var j = 0; j < nProjs - 1; j++) {\n addSegment(cp, bpts[_i * nProjs + j], bpts[_i * nProjs + j + 1], r.bezierProjPcts[j], r.bezierProjPcts[j + 1]);\n }\n\n addSegment(cp, bpts[_i * nProjs + nProjs - 1], cp.p2, r.bezierProjPcts[nProjs - 1], 1); // last\n }\n\n return createControlPointInfo.cache = ctrlpts;\n };\n\n var calculateEndProjection = function calculateEndProjection(prefix) {\n var angle;\n var isSrc = prefix === 'source';\n\n if (!content[prefix]) {\n return;\n }\n\n var offset = edge.pstyle(prefix + '-text-offset').pfValue;\n\n switch (rs.edgeType) {\n case 'self':\n case 'compound':\n case 'bezier':\n case 'multibezier':\n {\n var cps = createControlPointInfo();\n var selected;\n var startDist = 0;\n var totalDist = 0; // find the segment we're on\n\n for (var i = 0; i < cps.length; i++) {\n var _cp = cps[isSrc ? i : cps.length - 1 - i];\n\n for (var j = 0; j < _cp.segments.length; j++) {\n var _seg = _cp.segments[isSrc ? j : _cp.segments.length - 1 - j];\n var lastSeg = i === cps.length - 1 && j === _cp.segments.length - 1;\n startDist = totalDist;\n totalDist += _seg.length;\n\n if (totalDist >= offset || lastSeg) {\n selected = {\n cp: _cp,\n segment: _seg\n };\n break;\n }\n }\n\n if (selected) {\n break;\n }\n }\n\n var cp = selected.cp;\n var seg = selected.segment;\n var tSegment = (offset - startDist) / seg.length;\n var segDt = seg.t1 - seg.t0;\n var t = isSrc ? seg.t0 + segDt * tSegment : seg.t1 - segDt * tSegment;\n t = bound(0, t, 1);\n p = qbezierPtAt(cp.p0, cp.p1, cp.p2, t);\n angle = bezierAngle(cp.p0, cp.p1, cp.p2, t);\n break;\n }\n\n case 'straight':\n case 'segments':\n case 'haystack':\n {\n var d = 0,\n di,\n d0;\n var p0, p1;\n var l = rs.allpts.length;\n\n for (var _i2 = 0; _i2 + 3 < l; _i2 += 2) {\n if (isSrc) {\n p0 = {\n x: rs.allpts[_i2],\n y: rs.allpts[_i2 + 1]\n };\n p1 = {\n x: rs.allpts[_i2 + 2],\n y: rs.allpts[_i2 + 3]\n };\n } else {\n p0 = {\n x: rs.allpts[l - 2 - _i2],\n y: rs.allpts[l - 1 - _i2]\n };\n p1 = {\n x: rs.allpts[l - 4 - _i2],\n y: rs.allpts[l - 3 - _i2]\n };\n }\n\n di = dist(p0, p1);\n d0 = d;\n d += di;\n\n if (d >= offset) {\n break;\n }\n }\n\n var pD = offset - d0;\n\n var _t = pD / di;\n\n _t = bound(0, _t, 1);\n p = lineAt(p0, p1, _t);\n angle = lineAngle(p0, p1);\n break;\n }\n }\n\n setRs('labelX', prefix, p.x);\n setRs('labelY', prefix, p.y);\n setRs('labelAutoAngle', prefix, angle);\n };\n\n calculateEndProjection('source');\n calculateEndProjection('target');\n this.applyLabelDimensions(edge);\n};\n\nBRp$6.applyLabelDimensions = function (ele) {\n this.applyPrefixedLabelDimensions(ele);\n\n if (ele.isEdge()) {\n this.applyPrefixedLabelDimensions(ele, 'source');\n this.applyPrefixedLabelDimensions(ele, 'target');\n }\n};\n\nBRp$6.applyPrefixedLabelDimensions = function (ele, prefix) {\n var _p = ele._private;\n var text = this.getLabelText(ele, prefix);\n var labelDims = this.calculateLabelDimensions(ele, text);\n var lineHeight = ele.pstyle('line-height').pfValue;\n var textWrap = ele.pstyle('text-wrap').strValue;\n var lines = getPrefixedProperty(_p.rscratch, 'labelWrapCachedLines', prefix) || [];\n var numLines = textWrap !== 'wrap' ? 1 : Math.max(lines.length, 1);\n var normPerLineHeight = labelDims.height / numLines;\n var labelLineHeight = normPerLineHeight * lineHeight;\n var width = labelDims.width;\n var height = labelDims.height + (numLines - 1) * (lineHeight - 1) * normPerLineHeight;\n setPrefixedProperty(_p.rstyle, 'labelWidth', prefix, width);\n setPrefixedProperty(_p.rscratch, 'labelWidth', prefix, width);\n setPrefixedProperty(_p.rstyle, 'labelHeight', prefix, height);\n setPrefixedProperty(_p.rscratch, 'labelHeight', prefix, height);\n setPrefixedProperty(_p.rscratch, 'labelLineHeight', prefix, labelLineHeight);\n};\n\nBRp$6.getLabelText = function (ele, prefix) {\n var _p = ele._private;\n var pfd = prefix ? prefix + '-' : '';\n var text = ele.pstyle(pfd + 'label').strValue;\n var textTransform = ele.pstyle('text-transform').value;\n\n var rscratch = function rscratch(propName, value) {\n if (value) {\n setPrefixedProperty(_p.rscratch, propName, prefix, value);\n return value;\n } else {\n return getPrefixedProperty(_p.rscratch, propName, prefix);\n }\n }; // for empty text, skip all processing\n\n\n if (!text) {\n return '';\n }\n\n if (textTransform == 'none') ; else if (textTransform == 'uppercase') {\n text = text.toUpperCase();\n } else if (textTransform == 'lowercase') {\n text = text.toLowerCase();\n }\n\n var wrapStyle = ele.pstyle('text-wrap').value;\n\n if (wrapStyle === 'wrap') {\n var labelKey = rscratch('labelKey'); // save recalc if the label is the same as before\n\n if (labelKey != null && rscratch('labelWrapKey') === labelKey) {\n return rscratch('labelWrapCachedText');\n }\n\n var zwsp = \"\\u200B\";\n var lines = text.split('\\n');\n var maxW = ele.pstyle('text-max-width').pfValue;\n var overflow = ele.pstyle('text-overflow-wrap').value;\n var overflowAny = overflow === 'anywhere';\n var wrappedLines = [];\n var wordsRegex = /[\\s\\u200b]+/;\n var wordSeparator = overflowAny ? '' : ' ';\n\n for (var l = 0; l < lines.length; l++) {\n var line = lines[l];\n var lineDims = this.calculateLabelDimensions(ele, line);\n var lineW = lineDims.width;\n\n if (overflowAny) {\n var processedLine = line.split('').join(zwsp);\n line = processedLine;\n }\n\n if (lineW > maxW) {\n // line is too long\n var words = line.split(wordsRegex);\n var subline = '';\n\n for (var w = 0; w < words.length; w++) {\n var word = words[w];\n var testLine = subline.length === 0 ? word : subline + wordSeparator + word;\n var testDims = this.calculateLabelDimensions(ele, testLine);\n var testW = testDims.width;\n\n if (testW <= maxW) {\n // word fits on current line\n subline += word + wordSeparator;\n } else {\n // word starts new line\n if (subline) {\n wrappedLines.push(subline);\n }\n\n subline = word + wordSeparator;\n }\n } // if there's remaining text, put it in a wrapped line\n\n\n if (!subline.match(/^[\\s\\u200b]+$/)) {\n wrappedLines.push(subline);\n }\n } else {\n // line is already short enough\n wrappedLines.push(line);\n }\n } // for\n\n\n rscratch('labelWrapCachedLines', wrappedLines);\n text = rscratch('labelWrapCachedText', wrappedLines.join('\\n'));\n rscratch('labelWrapKey', labelKey);\n } else if (wrapStyle === 'ellipsis') {\n var _maxW = ele.pstyle('text-max-width').pfValue;\n var ellipsized = '';\n var ellipsis = \"\\u2026\";\n var incLastCh = false;\n\n if (this.calculateLabelDimensions(ele, text).width < _maxW) {\n // the label already fits\n return text;\n }\n\n for (var i = 0; i < text.length; i++) {\n var widthWithNextCh = this.calculateLabelDimensions(ele, ellipsized + text[i] + ellipsis).width;\n\n if (widthWithNextCh > _maxW) {\n break;\n }\n\n ellipsized += text[i];\n\n if (i === text.length - 1) {\n incLastCh = true;\n }\n }\n\n if (!incLastCh) {\n ellipsized += ellipsis;\n }\n\n return ellipsized;\n } // if ellipsize\n\n\n return text;\n};\n\nBRp$6.getLabelJustification = function (ele) {\n var justification = ele.pstyle('text-justification').strValue;\n var textHalign = ele.pstyle('text-halign').strValue;\n\n if (justification === 'auto') {\n if (ele.isNode()) {\n switch (textHalign) {\n case 'left':\n return 'right';\n\n case 'right':\n return 'left';\n\n default:\n return 'center';\n }\n } else {\n return 'center';\n }\n } else {\n return justification;\n }\n};\n\nBRp$6.calculateLabelDimensions = function (ele, text) {\n var r = this;\n var cacheKey = hashString(text, ele._private.labelDimsKey);\n var cache = r.labelDimCache || (r.labelDimCache = []);\n var existingVal = cache[cacheKey];\n\n if (existingVal != null) {\n return existingVal;\n }\n\n var padding = 0; // add padding around text dims, as the measurement isn't that accurate\n\n var fStyle = ele.pstyle('font-style').strValue;\n var size = ele.pstyle('font-size').pfValue;\n var family = ele.pstyle('font-family').strValue;\n var weight = ele.pstyle('font-weight').strValue;\n var canvas = this.labelCalcCanvas;\n var c2d = this.labelCalcCanvasContext;\n\n if (!canvas) {\n canvas = this.labelCalcCanvas = document.createElement('canvas');\n c2d = this.labelCalcCanvasContext = canvas.getContext('2d');\n var ds = canvas.style;\n ds.position = 'absolute';\n ds.left = '-9999px';\n ds.top = '-9999px';\n ds.zIndex = '-1';\n ds.visibility = 'hidden';\n ds.pointerEvents = 'none';\n }\n\n c2d.font = \"\".concat(fStyle, \" \").concat(weight, \" \").concat(size, \"px \").concat(family);\n var width = 0;\n var height = 0;\n var lines = text.split('\\n');\n\n for (var i = 0; i < lines.length; i++) {\n var line = lines[i];\n var metrics = c2d.measureText(line);\n var w = Math.ceil(metrics.width);\n var h = size;\n width = Math.max(w, width);\n height += h;\n }\n\n width += padding;\n height += padding;\n return cache[cacheKey] = {\n width: width,\n height: height\n };\n};\n\nBRp$6.calculateLabelAngle = function (ele, prefix) {\n var _p = ele._private;\n var rs = _p.rscratch;\n var isEdge = ele.isEdge();\n var prefixDash = prefix ? prefix + '-' : '';\n var rot = ele.pstyle(prefixDash + 'text-rotation');\n var rotStr = rot.strValue;\n\n if (rotStr === 'none') {\n return 0;\n } else if (isEdge && rotStr === 'autorotate') {\n return rs.labelAutoAngle;\n } else if (rotStr === 'autorotate') {\n return 0;\n } else {\n return rot.pfValue;\n }\n};\n\nBRp$6.calculateLabelAngles = function (ele) {\n var r = this;\n var isEdge = ele.isEdge();\n var _p = ele._private;\n var rs = _p.rscratch;\n rs.labelAngle = r.calculateLabelAngle(ele);\n\n if (isEdge) {\n rs.sourceLabelAngle = r.calculateLabelAngle(ele, 'source');\n rs.targetLabelAngle = r.calculateLabelAngle(ele, 'target');\n }\n};\n\nvar BRp$7 = {};\nvar TOO_SMALL_CUT_RECT = 28;\nvar warnedCutRect = false;\n\nBRp$7.getNodeShape = function (node) {\n var r = this;\n var shape = node.pstyle('shape').value;\n\n if (shape === 'cutrectangle' && (node.width() < TOO_SMALL_CUT_RECT || node.height() < TOO_SMALL_CUT_RECT)) {\n if (!warnedCutRect) {\n warn('The `cutrectangle` node shape can not be used at small sizes so `rectangle` is used instead');\n warnedCutRect = true;\n }\n\n return 'rectangle';\n }\n\n if (node.isParent()) {\n if (shape === 'rectangle' || shape === 'roundrectangle' || shape === 'round-rectangle' || shape === 'cutrectangle' || shape === 'cut-rectangle' || shape === 'barrel') {\n return shape;\n } else {\n return 'rectangle';\n }\n }\n\n if (shape === 'polygon') {\n var points = node.pstyle('shape-polygon-points').value;\n return r.nodeShapes.makePolygon(points).name;\n }\n\n return shape;\n};\n\nvar BRp$8 = {};\n\nBRp$8.registerCalculationListeners = function () {\n var cy = this.cy;\n var elesToUpdate = cy.collection();\n var r = this;\n\n var enqueue = function enqueue(eles) {\n var dirtyStyleCaches = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;\n elesToUpdate.merge(eles);\n\n if (dirtyStyleCaches) {\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n var _p = ele._private;\n var rstyle = _p.rstyle;\n rstyle.clean = false;\n rstyle.cleanConnected = false;\n }\n }\n };\n\n r.binder(cy).on('bounds.* dirty.*', function onDirtyBounds(e) {\n var ele = e.target;\n enqueue(ele);\n }).on('style.* background.*', function onDirtyStyle(e) {\n var ele = e.target;\n enqueue(ele, false);\n });\n\n var updateEleCalcs = function updateEleCalcs(willDraw) {\n if (willDraw) {\n var fns = r.onUpdateEleCalcsFns; // because we need to have up-to-date style (e.g. stylesheet mappers)\n // before calculating rendered style (and pstyle might not be called yet)\n\n elesToUpdate.cleanStyle();\n\n for (var i = 0; i < elesToUpdate.length; i++) {\n var ele = elesToUpdate[i];\n var rstyle = ele._private.rstyle;\n\n if (ele.isNode() && !rstyle.cleanConnected) {\n enqueue(ele.connectedEdges());\n rstyle.cleanConnected = true;\n }\n }\n\n if (fns) {\n for (var _i = 0; _i < fns.length; _i++) {\n var fn = fns[_i];\n fn(willDraw, elesToUpdate);\n }\n }\n\n r.recalculateRenderedStyle(elesToUpdate);\n elesToUpdate = cy.collection();\n }\n };\n\n r.flushRenderedStyleQueue = function () {\n updateEleCalcs(true);\n };\n\n r.beforeRender(updateEleCalcs, r.beforeRenderPriorities.eleCalcs);\n};\n\nBRp$8.onUpdateEleCalcs = function (fn) {\n var fns = this.onUpdateEleCalcsFns = this.onUpdateEleCalcsFns || [];\n fns.push(fn);\n};\n\nBRp$8.recalculateRenderedStyle = function (eles, useCache) {\n var isCleanConnected = function isCleanConnected(ele) {\n return ele._private.rstyle.cleanConnected;\n };\n\n var edges = [];\n var nodes = []; // the renderer can't be used for calcs when destroyed, e.g. ele.boundingBox()\n\n if (this.destroyed) {\n return;\n } // use cache by default for perf\n\n\n if (useCache === undefined) {\n useCache = true;\n }\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n var _p = ele._private;\n var rstyle = _p.rstyle; // an edge may be implicitly dirty b/c of one of its connected nodes\n // (and a request for recalc may come in between frames)\n\n if (ele.isEdge() && (!isCleanConnected(ele.source()) || !isCleanConnected(ele.target()))) {\n rstyle.clean = false;\n } // only update if dirty and in graph\n\n\n if (useCache && rstyle.clean || ele.removed()) {\n continue;\n } // only update if not display: none\n\n\n if (ele.pstyle('display').value === 'none') {\n continue;\n }\n\n if (_p.group === 'nodes') {\n nodes.push(ele);\n } else {\n // edges\n edges.push(ele);\n }\n\n rstyle.clean = true;\n } // update node data from projections\n\n\n for (var _i2 = 0; _i2 < nodes.length; _i2++) {\n var _ele = nodes[_i2];\n var _p2 = _ele._private;\n var _rstyle = _p2.rstyle;\n\n var pos = _ele.position();\n\n this.recalculateNodeLabelProjection(_ele);\n _rstyle.nodeX = pos.x;\n _rstyle.nodeY = pos.y;\n _rstyle.nodeW = _ele.pstyle('width').pfValue;\n _rstyle.nodeH = _ele.pstyle('height').pfValue;\n }\n\n this.recalculateEdgeProjections(edges); // update edge data from projections\n\n for (var _i3 = 0; _i3 < edges.length; _i3++) {\n var _ele2 = edges[_i3];\n var _p3 = _ele2._private;\n var _rstyle2 = _p3.rstyle;\n var rs = _p3.rscratch; // update rstyle positions\n\n _rstyle2.srcX = rs.arrowStartX;\n _rstyle2.srcY = rs.arrowStartY;\n _rstyle2.tgtX = rs.arrowEndX;\n _rstyle2.tgtY = rs.arrowEndY;\n _rstyle2.midX = rs.midX;\n _rstyle2.midY = rs.midY;\n _rstyle2.labelAngle = rs.labelAngle;\n _rstyle2.sourceLabelAngle = rs.sourceLabelAngle;\n _rstyle2.targetLabelAngle = rs.targetLabelAngle;\n }\n};\n\nvar BRp$9 = {};\n\nBRp$9.updateCachedGrabbedEles = function () {\n var eles = this.cachedZSortedEles;\n\n if (!eles) {\n // just let this be recalculated on the next z sort tick\n return;\n }\n\n eles.drag = [];\n eles.nondrag = [];\n var grabTargets = [];\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n var rs = ele._private.rscratch;\n\n if (ele.grabbed() && !ele.isParent()) {\n grabTargets.push(ele);\n } else if (rs.inDragLayer) {\n eles.drag.push(ele);\n } else {\n eles.nondrag.push(ele);\n }\n } // put the grab target nodes last so it's on top of its neighbourhood\n\n\n for (var i = 0; i < grabTargets.length; i++) {\n var ele = grabTargets[i];\n eles.drag.push(ele);\n }\n};\n\nBRp$9.invalidateCachedZSortedEles = function () {\n this.cachedZSortedEles = null;\n};\n\nBRp$9.getCachedZSortedEles = function (forceRecalc) {\n if (forceRecalc || !this.cachedZSortedEles) {\n var eles = this.cy.mutableElements().toArray();\n eles.sort(zIndexSort);\n eles.interactive = eles.filter(function (ele) {\n return ele.interactive();\n });\n this.cachedZSortedEles = eles;\n this.updateCachedGrabbedEles();\n } else {\n eles = this.cachedZSortedEles;\n }\n\n return eles;\n};\n\nvar BRp$a = {};\n[BRp$1, BRp$2, BRp$3, BRp$4, BRp$5, BRp$6, BRp$7, BRp$8, BRp$9].forEach(function (props) {\n extend(BRp$a, props);\n});\n\nvar BRp$b = {};\n\nBRp$b.getCachedImage = function (url, crossOrigin, onLoad) {\n var r = this;\n var imageCache = r.imageCache = r.imageCache || {};\n var cache = imageCache[url];\n\n if (cache) {\n if (!cache.image.complete) {\n cache.image.addEventListener('load', onLoad);\n }\n\n return cache.image;\n } else {\n cache = imageCache[url] = imageCache[url] || {};\n var image = cache.image = new Image(); // eslint-disable-line no-undef\n\n image.addEventListener('load', onLoad);\n image.addEventListener('error', function () {\n image.error = true;\n }); // #1582 safari doesn't load data uris with crossOrigin properly\n // https://bugs.webkit.org/show_bug.cgi?id=123978\n\n var dataUriPrefix = 'data:';\n var isDataUri = url.substring(0, dataUriPrefix.length).toLowerCase() === dataUriPrefix;\n\n if (!isDataUri) {\n image.crossOrigin = crossOrigin; // prevent tainted canvas\n }\n\n image.src = url;\n return image;\n }\n};\n\nvar BRp$c = {};\n/* global document, window, ResizeObserver, MutationObserver */\n\nBRp$c.registerBinding = function (target, event, handler, useCapture) {\n // eslint-disable-line no-unused-vars\n var args = Array.prototype.slice.apply(arguments, [1]); // copy\n\n var b = this.binder(target);\n return b.on.apply(b, args);\n};\n\nBRp$c.binder = function (tgt) {\n var r = this;\n var tgtIsDom = tgt === window || tgt === document || tgt === document.body || domElement(tgt);\n\n if (r.supportsPassiveEvents == null) {\n // from https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection\n var supportsPassive = false;\n\n try {\n var opts = Object.defineProperty({}, 'passive', {\n get: function get() {\n supportsPassive = true;\n return true;\n }\n });\n window.addEventListener('test', null, opts);\n } catch (err) {// not supported\n }\n\n r.supportsPassiveEvents = supportsPassive;\n }\n\n var on = function on(event, handler, useCapture) {\n var args = Array.prototype.slice.call(arguments);\n\n if (tgtIsDom && r.supportsPassiveEvents) {\n // replace useCapture w/ opts obj\n args[2] = {\n capture: useCapture != null ? useCapture : false,\n passive: false,\n once: false\n };\n }\n\n r.bindings.push({\n target: tgt,\n args: args\n });\n (tgt.addEventListener || tgt.on).apply(tgt, args);\n return this;\n };\n\n return {\n on: on,\n addEventListener: on,\n addListener: on,\n bind: on\n };\n};\n\nBRp$c.nodeIsDraggable = function (node) {\n return node && node.isNode() && !node.locked() && node.grabbable();\n};\n\nBRp$c.nodeIsGrabbable = function (node) {\n return this.nodeIsDraggable(node) && node.interactive();\n};\n\nBRp$c.load = function () {\n var r = this;\n\n var isSelected = function isSelected(ele) {\n return ele.selected();\n };\n\n var triggerEvents = function triggerEvents(target, names, e, position) {\n if (target == null) {\n target = r.cy;\n }\n\n for (var i = 0; i < names.length; i++) {\n var name = names[i];\n target.emit({\n originalEvent: e,\n type: name,\n position: position\n });\n }\n };\n\n var isMultSelKeyDown = function isMultSelKeyDown(e) {\n return e.shiftKey || e.metaKey || e.ctrlKey; // maybe e.altKey\n };\n\n var allowPanningPassthrough = function allowPanningPassthrough(down, downs) {\n var allowPassthrough = true;\n\n if (r.cy.hasCompoundNodes() && down && down.pannable()) {\n // a grabbable compound node below the ele => no passthrough panning\n for (var i = 0; downs && i < downs.length; i++) {\n var down = downs[i]; //if any parent node in event hierarchy isn't pannable, reject passthrough\n\n if (down.isNode() && down.isParent() && !down.pannable()) {\n allowPassthrough = false;\n break;\n }\n }\n } else {\n allowPassthrough = true;\n }\n\n return allowPassthrough;\n };\n\n var setGrabbed = function setGrabbed(ele) {\n ele[0]._private.grabbed = true;\n };\n\n var setFreed = function setFreed(ele) {\n ele[0]._private.grabbed = false;\n };\n\n var setInDragLayer = function setInDragLayer(ele) {\n ele[0]._private.rscratch.inDragLayer = true;\n };\n\n var setOutDragLayer = function setOutDragLayer(ele) {\n ele[0]._private.rscratch.inDragLayer = false;\n };\n\n var setGrabTarget = function setGrabTarget(ele) {\n ele[0]._private.rscratch.isGrabTarget = true;\n };\n\n var removeGrabTarget = function removeGrabTarget(ele) {\n ele[0]._private.rscratch.isGrabTarget = false;\n };\n\n var addToDragList = function addToDragList(ele, opts) {\n var list = opts.addToList;\n var listHasEle = list.has(ele);\n\n if (!listHasEle) {\n list.merge(ele);\n setGrabbed(ele);\n }\n }; // helper function to determine which child nodes and inner edges\n // of a compound node to be dragged as well as the grabbed and selected nodes\n\n\n var addDescendantsToDrag = function addDescendantsToDrag(node, opts) {\n if (!node.cy().hasCompoundNodes()) {\n return;\n }\n\n if (opts.inDragLayer == null && opts.addToList == null) {\n return;\n } // nothing to do\n\n\n var innerNodes = node.descendants();\n\n if (opts.inDragLayer) {\n innerNodes.forEach(setInDragLayer);\n innerNodes.connectedEdges().forEach(setInDragLayer);\n }\n\n if (opts.addToList) {\n opts.addToList.unmerge(innerNodes);\n }\n }; // adds the given nodes and its neighbourhood to the drag layer\n\n\n var addNodesToDrag = function addNodesToDrag(nodes, opts) {\n opts = opts || {};\n var hasCompoundNodes = nodes.cy().hasCompoundNodes();\n\n if (opts.inDragLayer) {\n nodes.forEach(setInDragLayer);\n nodes.neighborhood().stdFilter(function (ele) {\n return !hasCompoundNodes || ele.isEdge();\n }).forEach(setInDragLayer);\n }\n\n if (opts.addToList) {\n nodes.forEach(function (ele) {\n addToDragList(ele, opts);\n });\n }\n\n addDescendantsToDrag(nodes, opts); // always add to drag\n // also add nodes and edges related to the topmost ancestor\n\n updateAncestorsInDragLayer(nodes, {\n inDragLayer: opts.inDragLayer\n });\n r.updateCachedGrabbedEles();\n };\n\n var addNodeToDrag = addNodesToDrag;\n\n var freeDraggedElements = function freeDraggedElements(grabbedEles) {\n if (!grabbedEles) {\n return;\n } // just go over all elements rather than doing a bunch of (possibly expensive) traversals\n\n\n r.getCachedZSortedEles().forEach(function (ele) {\n setFreed(ele);\n setOutDragLayer(ele);\n removeGrabTarget(ele);\n });\n r.updateCachedGrabbedEles();\n }; // helper function to determine which ancestor nodes and edges should go\n // to the drag layer (or should be removed from drag layer).\n\n\n var updateAncestorsInDragLayer = function updateAncestorsInDragLayer(node, opts) {\n if (opts.inDragLayer == null && opts.addToList == null) {\n return;\n } // nothing to do\n\n\n if (!node.cy().hasCompoundNodes()) {\n return;\n } // find top-level parent\n\n\n var parent = node.ancestors().orphans(); // no parent node: no nodes to add to the drag layer\n\n if (parent.same(node)) {\n return;\n }\n\n var nodes = parent.descendants().spawnSelf().merge(parent).unmerge(node).unmerge(node.descendants());\n var edges = nodes.connectedEdges();\n\n if (opts.inDragLayer) {\n edges.forEach(setInDragLayer);\n nodes.forEach(setInDragLayer);\n }\n\n if (opts.addToList) {\n nodes.forEach(function (ele) {\n addToDragList(ele, opts);\n });\n }\n };\n\n var blurActiveDomElement = function blurActiveDomElement() {\n if (document.activeElement != null && document.activeElement.blur != null) {\n document.activeElement.blur();\n }\n };\n\n var haveMutationsApi = typeof MutationObserver !== 'undefined';\n var haveResizeObserverApi = typeof ResizeObserver !== 'undefined'; // watch for when the cy container is removed from the dom\n\n if (haveMutationsApi) {\n r.removeObserver = new MutationObserver(function (mutns) {\n // eslint-disable-line no-undef\n for (var i = 0; i < mutns.length; i++) {\n var mutn = mutns[i];\n var rNodes = mutn.removedNodes;\n\n if (rNodes) {\n for (var j = 0; j < rNodes.length; j++) {\n var rNode = rNodes[j];\n\n if (rNode === r.container) {\n r.destroy();\n break;\n }\n }\n }\n }\n });\n\n if (r.container.parentNode) {\n r.removeObserver.observe(r.container.parentNode, {\n childList: true\n });\n }\n } else {\n r.registerBinding(r.container, 'DOMNodeRemoved', function (e) {\n // eslint-disable-line no-unused-vars\n r.destroy();\n });\n }\n\n var onResize = util(function () {\n r.cy.resize();\n }, 100);\n\n if (haveMutationsApi) {\n r.styleObserver = new MutationObserver(onResize); // eslint-disable-line no-undef\n\n r.styleObserver.observe(r.container, {\n attributes: true\n });\n } // auto resize\n\n\n r.registerBinding(window, 'resize', onResize); // eslint-disable-line no-undef\n\n if (haveResizeObserverApi) {\n r.resizeObserver = new ResizeObserver(onResize); // eslint-disable-line no-undef\n\n r.resizeObserver.observe(r.container);\n }\n\n var forEachUp = function forEachUp(domEle, fn) {\n while (domEle != null) {\n fn(domEle);\n domEle = domEle.parentNode;\n }\n };\n\n var invalidateCoords = function invalidateCoords() {\n r.invalidateContainerClientCoordsCache();\n };\n\n forEachUp(r.container, function (domEle) {\n r.registerBinding(domEle, 'transitionend', invalidateCoords);\n r.registerBinding(domEle, 'animationend', invalidateCoords);\n r.registerBinding(domEle, 'scroll', invalidateCoords);\n }); // stop right click menu from appearing on cy\n\n r.registerBinding(r.container, 'contextmenu', function (e) {\n e.preventDefault();\n });\n\n var inBoxSelection = function inBoxSelection() {\n return r.selection[4] !== 0;\n };\n\n var eventInContainer = function eventInContainer(e) {\n // save cycles if mouse events aren't to be captured\n var containerPageCoords = r.findContainerClientCoords();\n var x = containerPageCoords[0];\n var y = containerPageCoords[1];\n var width = containerPageCoords[2];\n var height = containerPageCoords[3];\n var positions = e.touches ? e.touches : [e];\n var atLeastOnePosInside = false;\n\n for (var i = 0; i < positions.length; i++) {\n var p = positions[i];\n\n if (x <= p.clientX && p.clientX <= x + width && y <= p.clientY && p.clientY <= y + height) {\n atLeastOnePosInside = true;\n break;\n }\n }\n\n if (!atLeastOnePosInside) {\n return false;\n }\n\n var container = r.container;\n var target = e.target;\n var tParent = target.parentNode;\n var containerIsTarget = false;\n\n while (tParent) {\n if (tParent === container) {\n containerIsTarget = true;\n break;\n }\n\n tParent = tParent.parentNode;\n }\n\n if (!containerIsTarget) {\n return false;\n } // if target is outisde cy container, then this event is not for us\n\n\n return true;\n }; // Primary key\n\n\n r.registerBinding(r.container, 'mousedown', function mousedownHandler(e) {\n if (!eventInContainer(e)) {\n return;\n }\n\n e.preventDefault();\n blurActiveDomElement();\n r.hoverData.capture = true;\n r.hoverData.which = e.which;\n var cy = r.cy;\n var gpos = [e.clientX, e.clientY];\n var pos = r.projectIntoViewport(gpos[0], gpos[1]);\n var select = r.selection;\n var nears = r.findNearestElements(pos[0], pos[1], true, false);\n var near = nears[0];\n var draggedElements = r.dragData.possibleDragElements;\n r.hoverData.mdownPos = pos;\n r.hoverData.mdownGPos = gpos;\n\n var checkForTaphold = function checkForTaphold() {\n r.hoverData.tapholdCancelled = false;\n clearTimeout(r.hoverData.tapholdTimeout);\n r.hoverData.tapholdTimeout = setTimeout(function () {\n if (r.hoverData.tapholdCancelled) {\n return;\n } else {\n var ele = r.hoverData.down;\n\n if (ele) {\n ele.emit({\n originalEvent: e,\n type: 'taphold',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n } else {\n cy.emit({\n originalEvent: e,\n type: 'taphold',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }\n }\n }, r.tapholdDuration);\n }; // Right click button\n\n\n if (e.which == 3) {\n r.hoverData.cxtStarted = true;\n var cxtEvt = {\n originalEvent: e,\n type: 'cxttapstart',\n position: {\n x: pos[0],\n y: pos[1]\n }\n };\n\n if (near) {\n near.activate();\n near.emit(cxtEvt);\n r.hoverData.down = near;\n } else {\n cy.emit(cxtEvt);\n }\n\n r.hoverData.downTime = new Date().getTime();\n r.hoverData.cxtDragged = false; // Primary button\n } else if (e.which == 1) {\n if (near) {\n near.activate();\n } // Element dragging\n\n\n {\n // If something is under the cursor and it is draggable, prepare to grab it\n if (near != null) {\n if (r.nodeIsGrabbable(near)) {\n var makeEvent = function makeEvent(type) {\n return {\n originalEvent: e,\n type: type,\n position: {\n x: pos[0],\n y: pos[1]\n }\n };\n };\n\n var triggerGrab = function triggerGrab(ele) {\n ele.emit(makeEvent('grab'));\n };\n\n setGrabTarget(near);\n\n if (!near.selected()) {\n draggedElements = r.dragData.possibleDragElements = cy.collection();\n addNodeToDrag(near, {\n addToList: draggedElements\n });\n near.emit(makeEvent('grabon')).emit(makeEvent('grab'));\n } else {\n draggedElements = r.dragData.possibleDragElements = cy.collection();\n var selectedNodes = cy.$(function (ele) {\n return ele.isNode() && ele.selected() && r.nodeIsGrabbable(ele);\n });\n addNodesToDrag(selectedNodes, {\n addToList: draggedElements\n });\n near.emit(makeEvent('grabon'));\n selectedNodes.forEach(triggerGrab);\n }\n\n r.redrawHint('eles', true);\n r.redrawHint('drag', true);\n }\n }\n\n r.hoverData.down = near;\n r.hoverData.downs = nears;\n r.hoverData.downTime = new Date().getTime();\n }\n triggerEvents(near, ['mousedown', 'tapstart', 'vmousedown'], e, {\n x: pos[0],\n y: pos[1]\n });\n\n if (near == null) {\n select[4] = 1;\n r.data.bgActivePosistion = {\n x: pos[0],\n y: pos[1]\n };\n r.redrawHint('select', true);\n r.redraw();\n } else if (near.pannable()) {\n select[4] = 1; // for future pan\n }\n\n checkForTaphold();\n } // Initialize selection box coordinates\n\n\n select[0] = select[2] = pos[0];\n select[1] = select[3] = pos[1];\n }, false);\n r.registerBinding(window, 'mousemove', function mousemoveHandler(e) {\n // eslint-disable-line no-undef\n var capture = r.hoverData.capture;\n\n if (!capture && !eventInContainer(e)) {\n return;\n }\n\n var preventDefault = false;\n var cy = r.cy;\n var zoom = cy.zoom();\n var gpos = [e.clientX, e.clientY];\n var pos = r.projectIntoViewport(gpos[0], gpos[1]);\n var mdownPos = r.hoverData.mdownPos;\n var mdownGPos = r.hoverData.mdownGPos;\n var select = r.selection;\n var near = null;\n\n if (!r.hoverData.draggingEles && !r.hoverData.dragging && !r.hoverData.selecting) {\n near = r.findNearestElement(pos[0], pos[1], true, false);\n }\n\n var last = r.hoverData.last;\n var down = r.hoverData.down;\n var disp = [pos[0] - select[2], pos[1] - select[3]];\n var draggedElements = r.dragData.possibleDragElements;\n var isOverThresholdDrag;\n\n if (mdownGPos) {\n var dx = gpos[0] - mdownGPos[0];\n var dx2 = dx * dx;\n var dy = gpos[1] - mdownGPos[1];\n var dy2 = dy * dy;\n var dist2 = dx2 + dy2;\n r.hoverData.isOverThresholdDrag = isOverThresholdDrag = dist2 >= r.desktopTapThreshold2;\n }\n\n var multSelKeyDown = isMultSelKeyDown(e);\n\n if (isOverThresholdDrag) {\n r.hoverData.tapholdCancelled = true;\n }\n\n var updateDragDelta = function updateDragDelta() {\n var dragDelta = r.hoverData.dragDelta = r.hoverData.dragDelta || [];\n\n if (dragDelta.length === 0) {\n dragDelta.push(disp[0]);\n dragDelta.push(disp[1]);\n } else {\n dragDelta[0] += disp[0];\n dragDelta[1] += disp[1];\n }\n };\n\n preventDefault = true;\n triggerEvents(near, ['mousemove', 'vmousemove', 'tapdrag'], e, {\n x: pos[0],\n y: pos[1]\n });\n\n var goIntoBoxMode = function goIntoBoxMode() {\n r.data.bgActivePosistion = undefined;\n\n if (!r.hoverData.selecting) {\n cy.emit({\n originalEvent: e,\n type: 'boxstart',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }\n\n select[4] = 1;\n r.hoverData.selecting = true;\n r.redrawHint('select', true);\n r.redraw();\n }; // trigger context drag if rmouse down\n\n\n if (r.hoverData.which === 3) {\n // but only if over threshold\n if (isOverThresholdDrag) {\n var cxtEvt = {\n originalEvent: e,\n type: 'cxtdrag',\n position: {\n x: pos[0],\n y: pos[1]\n }\n };\n\n if (down) {\n down.emit(cxtEvt);\n } else {\n cy.emit(cxtEvt);\n }\n\n r.hoverData.cxtDragged = true;\n\n if (!r.hoverData.cxtOver || near !== r.hoverData.cxtOver) {\n if (r.hoverData.cxtOver) {\n r.hoverData.cxtOver.emit({\n originalEvent: e,\n type: 'cxtdragout',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }\n\n r.hoverData.cxtOver = near;\n\n if (near) {\n near.emit({\n originalEvent: e,\n type: 'cxtdragover',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }\n }\n } // Check if we are drag panning the entire graph\n\n } else if (r.hoverData.dragging) {\n preventDefault = true;\n\n if (cy.panningEnabled() && cy.userPanningEnabled()) {\n var deltaP;\n\n if (r.hoverData.justStartedPan) {\n var mdPos = r.hoverData.mdownPos;\n deltaP = {\n x: (pos[0] - mdPos[0]) * zoom,\n y: (pos[1] - mdPos[1]) * zoom\n };\n r.hoverData.justStartedPan = false;\n } else {\n deltaP = {\n x: disp[0] * zoom,\n y: disp[1] * zoom\n };\n }\n\n cy.panBy(deltaP);\n r.hoverData.dragged = true;\n } // Needs reproject due to pan changing viewport\n\n\n pos = r.projectIntoViewport(e.clientX, e.clientY); // Checks primary button down & out of time & mouse not moved much\n } else if (select[4] == 1 && (down == null || down.pannable())) {\n if (isOverThresholdDrag) {\n if (!r.hoverData.dragging && cy.boxSelectionEnabled() && (multSelKeyDown || !cy.panningEnabled() || !cy.userPanningEnabled())) {\n goIntoBoxMode();\n } else if (!r.hoverData.selecting && cy.panningEnabled() && cy.userPanningEnabled()) {\n var allowPassthrough = allowPanningPassthrough(down, r.hoverData.downs);\n\n if (allowPassthrough) {\n r.hoverData.dragging = true;\n r.hoverData.justStartedPan = true;\n select[4] = 0;\n r.data.bgActivePosistion = array2point(mdownPos);\n r.redrawHint('select', true);\n r.redraw();\n }\n }\n\n if (down && down.pannable() && down.active()) {\n down.unactivate();\n }\n }\n } else {\n if (down && down.pannable() && down.active()) {\n down.unactivate();\n }\n\n if ((!down || !down.grabbed()) && near != last) {\n if (last) {\n triggerEvents(last, ['mouseout', 'tapdragout'], e, {\n x: pos[0],\n y: pos[1]\n });\n }\n\n if (near) {\n triggerEvents(near, ['mouseover', 'tapdragover'], e, {\n x: pos[0],\n y: pos[1]\n });\n }\n\n r.hoverData.last = near;\n }\n\n if (down) {\n if (isOverThresholdDrag) {\n // then we can take action\n if (cy.boxSelectionEnabled() && multSelKeyDown) {\n // then selection overrides\n if (down && down.grabbed()) {\n freeDraggedElements(draggedElements);\n down.emit('freeon');\n draggedElements.emit('free');\n\n if (r.dragData.didDrag) {\n down.emit('dragfreeon');\n draggedElements.emit('dragfree');\n }\n }\n\n goIntoBoxMode();\n } else if (down && down.grabbed() && r.nodeIsDraggable(down)) {\n // drag node\n var justStartedDrag = !r.dragData.didDrag;\n\n if (justStartedDrag) {\n r.redrawHint('eles', true);\n }\n\n r.dragData.didDrag = true; // indicate that we actually did drag the node\n\n var toTrigger = cy.collection(); // now, add the elements to the drag layer if not done already\n\n if (!r.hoverData.draggingEles) {\n addNodesToDrag(draggedElements, {\n inDragLayer: true\n });\n }\n\n var totalShift = {\n x: 0,\n y: 0\n };\n\n if (number(disp[0]) && number(disp[1])) {\n totalShift.x += disp[0];\n totalShift.y += disp[1];\n\n if (justStartedDrag) {\n var dragDelta = r.hoverData.dragDelta;\n\n if (dragDelta && number(dragDelta[0]) && number(dragDelta[1])) {\n totalShift.x += dragDelta[0];\n totalShift.y += dragDelta[1];\n }\n }\n }\n\n for (var i = 0; i < draggedElements.length; i++) {\n var dEle = draggedElements[i];\n\n if (r.nodeIsDraggable(dEle) && dEle.grabbed()) {\n toTrigger.push(dEle);\n }\n }\n\n r.hoverData.draggingEles = true;\n toTrigger.silentShift(totalShift).emit('position drag');\n r.redrawHint('drag', true);\n r.redraw();\n }\n } else {\n // otherwise save drag delta for when we actually start dragging so the relative grab pos is constant\n updateDragDelta();\n }\n } // prevent the dragging from triggering text selection on the page\n\n\n preventDefault = true;\n }\n\n select[2] = pos[0];\n select[3] = pos[1];\n\n if (preventDefault) {\n if (e.stopPropagation) e.stopPropagation();\n if (e.preventDefault) e.preventDefault();\n return false;\n }\n }, false);\n r.registerBinding(window, 'mouseup', function mouseupHandler(e) {\n // eslint-disable-line no-undef\n var capture = r.hoverData.capture;\n\n if (!capture) {\n return;\n }\n\n r.hoverData.capture = false;\n var cy = r.cy;\n var pos = r.projectIntoViewport(e.clientX, e.clientY);\n var select = r.selection;\n var near = r.findNearestElement(pos[0], pos[1], true, false);\n var draggedElements = r.dragData.possibleDragElements;\n var down = r.hoverData.down;\n var multSelKeyDown = isMultSelKeyDown(e);\n\n if (r.data.bgActivePosistion) {\n r.redrawHint('select', true);\n r.redraw();\n }\n\n r.hoverData.tapholdCancelled = true;\n r.data.bgActivePosistion = undefined; // not active bg now\n\n if (down) {\n down.unactivate();\n }\n\n if (r.hoverData.which === 3) {\n var cxtEvt = {\n originalEvent: e,\n type: 'cxttapend',\n position: {\n x: pos[0],\n y: pos[1]\n }\n };\n\n if (down) {\n down.emit(cxtEvt);\n } else {\n cy.emit(cxtEvt);\n }\n\n if (!r.hoverData.cxtDragged) {\n var cxtTap = {\n originalEvent: e,\n type: 'cxttap',\n position: {\n x: pos[0],\n y: pos[1]\n }\n };\n\n if (down) {\n down.emit(cxtTap);\n } else {\n cy.emit(cxtTap);\n }\n }\n\n r.hoverData.cxtDragged = false;\n r.hoverData.which = null;\n } else if (r.hoverData.which === 1) {\n triggerEvents(near, ['mouseup', 'tapend', 'vmouseup'], e, {\n x: pos[0],\n y: pos[1]\n });\n\n if (!r.dragData.didDrag // didn't move a node around\n && !r.hoverData.dragged // didn't pan\n && !r.hoverData.selecting // not box selection\n && !r.hoverData.isOverThresholdDrag // didn't move too much\n ) {\n triggerEvents(down, ['click', 'tap', 'vclick'], e, {\n x: pos[0],\n y: pos[1]\n });\n } // Deselect all elements if nothing is currently under the mouse cursor and we aren't dragging something\n\n\n if (down == null && // not mousedown on node\n !r.dragData.didDrag // didn't move the node around\n && !r.hoverData.selecting // not box selection\n && !r.hoverData.dragged // didn't pan\n && !isMultSelKeyDown(e)) {\n cy.$(isSelected).unselect(['tapunselect']);\n\n if (draggedElements.length > 0) {\n r.redrawHint('eles', true);\n }\n\n r.dragData.possibleDragElements = draggedElements = cy.collection();\n } // Single selection\n\n\n if (near == down && !r.dragData.didDrag && !r.hoverData.selecting) {\n if (near != null && near._private.selectable) {\n if (r.hoverData.dragging) ; else if (cy.selectionType() === 'additive' || multSelKeyDown) {\n if (near.selected()) {\n near.unselect(['tapunselect']);\n } else {\n near.select(['tapselect']);\n }\n } else {\n if (!multSelKeyDown) {\n cy.$(isSelected).unmerge(near).unselect(['tapunselect']);\n near.select(['tapselect']);\n }\n }\n\n r.redrawHint('eles', true);\n }\n }\n\n if (r.hoverData.selecting) {\n var box = cy.collection(r.getAllInBox(select[0], select[1], select[2], select[3]));\n r.redrawHint('select', true);\n\n if (box.length > 0) {\n r.redrawHint('eles', true);\n }\n\n cy.emit({\n type: 'boxend',\n originalEvent: e,\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n\n var eleWouldBeSelected = function eleWouldBeSelected(ele) {\n return ele.selectable() && !ele.selected();\n };\n\n if (cy.selectionType() === 'additive') {\n box.emit('box').stdFilter(eleWouldBeSelected).select().emit('boxselect');\n } else {\n if (!multSelKeyDown) {\n cy.$(isSelected).unmerge(box).unselect();\n }\n\n box.emit('box').stdFilter(eleWouldBeSelected).select().emit('boxselect');\n } // always need redraw in case eles unselectable\n\n\n r.redraw();\n } // Cancel drag pan\n\n\n if (r.hoverData.dragging) {\n r.hoverData.dragging = false;\n r.redrawHint('select', true);\n r.redrawHint('eles', true);\n r.redraw();\n }\n\n if (!select[4]) {\n r.redrawHint('drag', true);\n r.redrawHint('eles', true);\n var downWasGrabbed = down && down.grabbed();\n freeDraggedElements(draggedElements);\n\n if (downWasGrabbed) {\n down.emit('freeon');\n draggedElements.emit('free');\n\n if (r.dragData.didDrag) {\n down.emit('dragfreeon');\n draggedElements.emit('dragfree');\n }\n }\n }\n } // else not right mouse\n\n\n select[4] = 0;\n r.hoverData.down = null;\n r.hoverData.cxtStarted = false;\n r.hoverData.draggingEles = false;\n r.hoverData.selecting = false;\n r.hoverData.isOverThresholdDrag = false;\n r.dragData.didDrag = false;\n r.hoverData.dragged = false;\n r.hoverData.dragDelta = [];\n r.hoverData.mdownPos = null;\n r.hoverData.mdownGPos = null;\n }, false);\n\n var wheelHandler = function wheelHandler(e) {\n if (r.scrollingPage) {\n return;\n } // while scrolling, ignore wheel-to-zoom\n\n\n var cy = r.cy;\n var zoom = cy.zoom();\n var pan = cy.pan();\n var pos = r.projectIntoViewport(e.clientX, e.clientY);\n var rpos = [pos[0] * zoom + pan.x, pos[1] * zoom + pan.y];\n\n if (r.hoverData.draggingEles || r.hoverData.dragging || r.hoverData.cxtStarted || inBoxSelection()) {\n // if pan dragging or cxt dragging, wheel movements make no zoom\n e.preventDefault();\n return;\n }\n\n if (cy.panningEnabled() && cy.userPanningEnabled() && cy.zoomingEnabled() && cy.userZoomingEnabled()) {\n e.preventDefault();\n r.data.wheelZooming = true;\n clearTimeout(r.data.wheelTimeout);\n r.data.wheelTimeout = setTimeout(function () {\n r.data.wheelZooming = false;\n r.redrawHint('eles', true);\n r.redraw();\n }, 150);\n var diff;\n\n if (e.deltaY != null) {\n diff = e.deltaY / -250;\n } else if (e.wheelDeltaY != null) {\n diff = e.wheelDeltaY / 1000;\n } else {\n diff = e.wheelDelta / 1000;\n }\n\n diff = diff * r.wheelSensitivity;\n var needsWheelFix = e.deltaMode === 1;\n\n if (needsWheelFix) {\n // fixes slow wheel events on ff/linux and ff/windows\n diff *= 33;\n }\n\n var newZoom = cy.zoom() * Math.pow(10, diff);\n\n if (e.type === 'gesturechange') {\n newZoom = r.gestureStartZoom * e.scale;\n }\n\n cy.zoom({\n level: newZoom,\n renderedPosition: {\n x: rpos[0],\n y: rpos[1]\n }\n });\n }\n }; // Functions to help with whether mouse wheel should trigger zooming\n // --\n\n\n r.registerBinding(r.container, 'wheel', wheelHandler, true); // disable nonstandard wheel events\n // r.registerBinding(r.container, 'mousewheel', wheelHandler, true);\n // r.registerBinding(r.container, 'DOMMouseScroll', wheelHandler, true);\n // r.registerBinding(r.container, 'MozMousePixelScroll', wheelHandler, true); // older firefox\n\n r.registerBinding(window, 'scroll', function scrollHandler(e) {\n // eslint-disable-line no-unused-vars\n r.scrollingPage = true;\n clearTimeout(r.scrollingPageTimeout);\n r.scrollingPageTimeout = setTimeout(function () {\n r.scrollingPage = false;\n }, 250);\n }, true); // desktop safari pinch to zoom start\n\n r.registerBinding(r.container, 'gesturestart', function gestureStartHandler(e) {\n r.gestureStartZoom = r.cy.zoom();\n\n if (!r.hasTouchStarted) {\n // don't affect touch devices like iphone\n e.preventDefault();\n }\n }, true);\n r.registerBinding(r.container, 'gesturechange', function (e) {\n if (!r.hasTouchStarted) {\n // don't affect touch devices like iphone\n wheelHandler(e);\n }\n }, true); // Functions to help with handling mouseout/mouseover on the Cytoscape container\n // Handle mouseout on Cytoscape container\n\n r.registerBinding(r.container, 'mouseout', function mouseOutHandler(e) {\n var pos = r.projectIntoViewport(e.clientX, e.clientY);\n r.cy.emit({\n originalEvent: e,\n type: 'mouseout',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }, false);\n r.registerBinding(r.container, 'mouseover', function mouseOverHandler(e) {\n var pos = r.projectIntoViewport(e.clientX, e.clientY);\n r.cy.emit({\n originalEvent: e,\n type: 'mouseover',\n position: {\n x: pos[0],\n y: pos[1]\n }\n });\n }, false);\n var f1x1, f1y1, f2x1, f2y1; // starting points for pinch-to-zoom\n\n var distance1, distance1Sq; // initial distance between finger 1 and finger 2 for pinch-to-zoom\n\n var center1, modelCenter1; // center point on start pinch to zoom\n\n var offsetLeft, offsetTop;\n var containerWidth, containerHeight;\n var twoFingersStartInside;\n\n var distance = function distance(x1, y1, x2, y2) {\n return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));\n };\n\n var distanceSq = function distanceSq(x1, y1, x2, y2) {\n return (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);\n };\n\n var touchstartHandler;\n r.registerBinding(r.container, 'touchstart', touchstartHandler = function touchstartHandler(e) {\n r.hasTouchStarted = true;\n\n if (!eventInContainer(e)) {\n return;\n }\n\n blurActiveDomElement();\n r.touchData.capture = true;\n r.data.bgActivePosistion = undefined;\n var cy = r.cy;\n var now = r.touchData.now;\n var earlier = r.touchData.earlier;\n\n if (e.touches[0]) {\n var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);\n now[0] = pos[0];\n now[1] = pos[1];\n }\n\n if (e.touches[1]) {\n var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY);\n now[2] = pos[0];\n now[3] = pos[1];\n }\n\n if (e.touches[2]) {\n var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY);\n now[4] = pos[0];\n now[5] = pos[1];\n } // record starting points for pinch-to-zoom\n\n\n if (e.touches[1]) {\n r.touchData.singleTouchMoved = true;\n freeDraggedElements(r.dragData.touchDragEles);\n var offsets = r.findContainerClientCoords();\n offsetLeft = offsets[0];\n offsetTop = offsets[1];\n containerWidth = offsets[2];\n containerHeight = offsets[3];\n f1x1 = e.touches[0].clientX - offsetLeft;\n f1y1 = e.touches[0].clientY - offsetTop;\n f2x1 = e.touches[1].clientX - offsetLeft;\n f2y1 = e.touches[1].clientY - offsetTop;\n twoFingersStartInside = 0 <= f1x1 && f1x1 <= containerWidth && 0 <= f2x1 && f2x1 <= containerWidth && 0 <= f1y1 && f1y1 <= containerHeight && 0 <= f2y1 && f2y1 <= containerHeight;\n var pan = cy.pan();\n var zoom = cy.zoom();\n distance1 = distance(f1x1, f1y1, f2x1, f2y1);\n distance1Sq = distanceSq(f1x1, f1y1, f2x1, f2y1);\n center1 = [(f1x1 + f2x1) / 2, (f1y1 + f2y1) / 2];\n modelCenter1 = [(center1[0] - pan.x) / zoom, (center1[1] - pan.y) / zoom]; // consider context tap\n\n var cxtDistThreshold = 200;\n var cxtDistThresholdSq = cxtDistThreshold * cxtDistThreshold;\n\n if (distance1Sq < cxtDistThresholdSq && !e.touches[2]) {\n var near1 = r.findNearestElement(now[0], now[1], true, true);\n var near2 = r.findNearestElement(now[2], now[3], true, true);\n\n if (near1 && near1.isNode()) {\n near1.activate().emit({\n originalEvent: e,\n type: 'cxttapstart',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n r.touchData.start = near1;\n } else if (near2 && near2.isNode()) {\n near2.activate().emit({\n originalEvent: e,\n type: 'cxttapstart',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n r.touchData.start = near2;\n } else {\n cy.emit({\n originalEvent: e,\n type: 'cxttapstart',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n\n if (r.touchData.start) {\n r.touchData.start._private.grabbed = false;\n }\n\n r.touchData.cxt = true;\n r.touchData.cxtDragged = false;\n r.data.bgActivePosistion = undefined;\n r.redraw();\n return;\n }\n }\n\n if (e.touches[2]) {\n // ignore\n // safari on ios pans the page otherwise (normally you should be able to preventdefault on touchmove...)\n if (cy.boxSelectionEnabled()) {\n e.preventDefault();\n }\n } else if (e.touches[1]) ; else if (e.touches[0]) {\n var nears = r.findNearestElements(now[0], now[1], true, true);\n var near = nears[0];\n\n if (near != null) {\n near.activate();\n r.touchData.start = near;\n r.touchData.starts = nears;\n\n if (r.nodeIsGrabbable(near)) {\n var draggedEles = r.dragData.touchDragEles = cy.collection();\n var selectedNodes = null;\n r.redrawHint('eles', true);\n r.redrawHint('drag', true);\n\n if (near.selected()) {\n // reset drag elements, since near will be added again\n selectedNodes = cy.$(function (ele) {\n return ele.selected() && r.nodeIsGrabbable(ele);\n });\n addNodesToDrag(selectedNodes, {\n addToList: draggedEles\n });\n } else {\n addNodeToDrag(near, {\n addToList: draggedEles\n });\n }\n\n setGrabTarget(near);\n\n var makeEvent = function makeEvent(type) {\n return {\n originalEvent: e,\n type: type,\n position: {\n x: now[0],\n y: now[1]\n }\n };\n };\n\n near.emit(makeEvent('grabon'));\n\n if (selectedNodes) {\n selectedNodes.forEach(function (n) {\n n.emit(makeEvent('grab'));\n });\n } else {\n near.emit(makeEvent('grab'));\n }\n }\n }\n\n triggerEvents(near, ['touchstart', 'tapstart', 'vmousedown'], e, {\n x: now[0],\n y: now[1]\n });\n\n if (near == null) {\n r.data.bgActivePosistion = {\n x: pos[0],\n y: pos[1]\n };\n r.redrawHint('select', true);\n r.redraw();\n } // Tap, taphold\n // -----\n\n\n r.touchData.singleTouchMoved = false;\n r.touchData.singleTouchStartTime = +new Date();\n clearTimeout(r.touchData.tapholdTimeout);\n r.touchData.tapholdTimeout = setTimeout(function () {\n if (r.touchData.singleTouchMoved === false && !r.pinching // if pinching, then taphold unselect shouldn't take effect\n && !r.touchData.selecting // box selection shouldn't allow taphold through\n ) {\n triggerEvents(r.touchData.start, ['taphold'], e, {\n x: now[0],\n y: now[1]\n });\n }\n }, r.tapholdDuration);\n }\n\n if (e.touches.length >= 1) {\n var sPos = r.touchData.startPosition = [];\n\n for (var i = 0; i < now.length; i++) {\n sPos[i] = earlier[i] = now[i];\n }\n\n var touch0 = e.touches[0];\n r.touchData.startGPosition = [touch0.clientX, touch0.clientY];\n }\n }, false);\n var touchmoveHandler;\n r.registerBinding(window, 'touchmove', touchmoveHandler = function touchmoveHandler(e) {\n // eslint-disable-line no-undef\n var capture = r.touchData.capture;\n\n if (!capture && !eventInContainer(e)) {\n return;\n }\n\n var select = r.selection;\n var cy = r.cy;\n var now = r.touchData.now;\n var earlier = r.touchData.earlier;\n var zoom = cy.zoom();\n\n if (e.touches[0]) {\n var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);\n now[0] = pos[0];\n now[1] = pos[1];\n }\n\n if (e.touches[1]) {\n var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY);\n now[2] = pos[0];\n now[3] = pos[1];\n }\n\n if (e.touches[2]) {\n var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY);\n now[4] = pos[0];\n now[5] = pos[1];\n }\n\n var startGPos = r.touchData.startGPosition;\n var isOverThresholdDrag;\n\n if (capture && e.touches[0] && startGPos) {\n var disp = [];\n\n for (var j = 0; j < now.length; j++) {\n disp[j] = now[j] - earlier[j];\n }\n\n var dx = e.touches[0].clientX - startGPos[0];\n var dx2 = dx * dx;\n var dy = e.touches[0].clientY - startGPos[1];\n var dy2 = dy * dy;\n var dist2 = dx2 + dy2;\n isOverThresholdDrag = dist2 >= r.touchTapThreshold2;\n } // context swipe cancelling\n\n\n if (capture && r.touchData.cxt) {\n e.preventDefault();\n var f1x2 = e.touches[0].clientX - offsetLeft,\n f1y2 = e.touches[0].clientY - offsetTop;\n var f2x2 = e.touches[1].clientX - offsetLeft,\n f2y2 = e.touches[1].clientY - offsetTop; // var distance2 = distance( f1x2, f1y2, f2x2, f2y2 );\n\n var distance2Sq = distanceSq(f1x2, f1y2, f2x2, f2y2);\n var factorSq = distance2Sq / distance1Sq;\n var distThreshold = 150;\n var distThresholdSq = distThreshold * distThreshold;\n var factorThreshold = 1.5;\n var factorThresholdSq = factorThreshold * factorThreshold; // cancel ctx gestures if the distance b/t the fingers increases\n\n if (factorSq >= factorThresholdSq || distance2Sq >= distThresholdSq) {\n r.touchData.cxt = false;\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n var cxtEvt = {\n originalEvent: e,\n type: 'cxttapend',\n position: {\n x: now[0],\n y: now[1]\n }\n };\n\n if (r.touchData.start) {\n r.touchData.start.unactivate().emit(cxtEvt);\n r.touchData.start = null;\n } else {\n cy.emit(cxtEvt);\n }\n }\n } // context swipe\n\n\n if (capture && r.touchData.cxt) {\n var cxtEvt = {\n originalEvent: e,\n type: 'cxtdrag',\n position: {\n x: now[0],\n y: now[1]\n }\n };\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n\n if (r.touchData.start) {\n r.touchData.start.emit(cxtEvt);\n } else {\n cy.emit(cxtEvt);\n }\n\n if (r.touchData.start) {\n r.touchData.start._private.grabbed = false;\n }\n\n r.touchData.cxtDragged = true;\n var near = r.findNearestElement(now[0], now[1], true, true);\n\n if (!r.touchData.cxtOver || near !== r.touchData.cxtOver) {\n if (r.touchData.cxtOver) {\n r.touchData.cxtOver.emit({\n originalEvent: e,\n type: 'cxtdragout',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n\n r.touchData.cxtOver = near;\n\n if (near) {\n near.emit({\n originalEvent: e,\n type: 'cxtdragover',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n } // box selection\n\n } else if (capture && e.touches[2] && cy.boxSelectionEnabled()) {\n e.preventDefault();\n r.data.bgActivePosistion = undefined;\n this.lastThreeTouch = +new Date();\n\n if (!r.touchData.selecting) {\n cy.emit({\n originalEvent: e,\n type: 'boxstart',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n\n r.touchData.selecting = true;\n r.touchData.didSelect = true;\n select[4] = 1;\n\n if (!select || select.length === 0 || select[0] === undefined) {\n select[0] = (now[0] + now[2] + now[4]) / 3;\n select[1] = (now[1] + now[3] + now[5]) / 3;\n select[2] = (now[0] + now[2] + now[4]) / 3 + 1;\n select[3] = (now[1] + now[3] + now[5]) / 3 + 1;\n } else {\n select[2] = (now[0] + now[2] + now[4]) / 3;\n select[3] = (now[1] + now[3] + now[5]) / 3;\n }\n\n r.redrawHint('select', true);\n r.redraw(); // pinch to zoom\n } else if (capture && e.touches[1] && !r.touchData.didSelect // don't allow box selection to degrade to pinch-to-zoom\n && cy.zoomingEnabled() && cy.panningEnabled() && cy.userZoomingEnabled() && cy.userPanningEnabled()) {\n // two fingers => pinch to zoom\n e.preventDefault();\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n var draggedEles = r.dragData.touchDragEles;\n\n if (draggedEles) {\n r.redrawHint('drag', true);\n\n for (var i = 0; i < draggedEles.length; i++) {\n var de_p = draggedEles[i]._private;\n de_p.grabbed = false;\n de_p.rscratch.inDragLayer = false;\n }\n }\n\n var _start = r.touchData.start; // (x2, y2) for fingers 1 and 2\n\n var f1x2 = e.touches[0].clientX - offsetLeft,\n f1y2 = e.touches[0].clientY - offsetTop;\n var f2x2 = e.touches[1].clientX - offsetLeft,\n f2y2 = e.touches[1].clientY - offsetTop;\n var distance2 = distance(f1x2, f1y2, f2x2, f2y2); // var distance2Sq = distanceSq( f1x2, f1y2, f2x2, f2y2 );\n // var factor = Math.sqrt( distance2Sq ) / Math.sqrt( distance1Sq );\n\n var factor = distance2 / distance1;\n\n if (twoFingersStartInside) {\n // delta finger1\n var df1x = f1x2 - f1x1;\n var df1y = f1y2 - f1y1; // delta finger 2\n\n var df2x = f2x2 - f2x1;\n var df2y = f2y2 - f2y1; // translation is the normalised vector of the two fingers movement\n // i.e. so pinching cancels out and moving together pans\n\n var tx = (df1x + df2x) / 2;\n var ty = (df1y + df2y) / 2; // now calculate the zoom\n\n var zoom1 = cy.zoom();\n var zoom2 = zoom1 * factor;\n var pan1 = cy.pan(); // the model center point converted to the current rendered pos\n\n var ctrx = modelCenter1[0] * zoom1 + pan1.x;\n var ctry = modelCenter1[1] * zoom1 + pan1.y;\n var pan2 = {\n x: -zoom2 / zoom1 * (ctrx - pan1.x - tx) + ctrx,\n y: -zoom2 / zoom1 * (ctry - pan1.y - ty) + ctry\n }; // remove dragged eles\n\n if (_start && _start.active()) {\n var draggedEles = r.dragData.touchDragEles;\n freeDraggedElements(draggedEles);\n r.redrawHint('drag', true);\n r.redrawHint('eles', true);\n\n _start.unactivate().emit('freeon');\n\n draggedEles.emit('free');\n\n if (r.dragData.didDrag) {\n _start.emit('dragfreeon');\n\n draggedEles.emit('dragfree');\n }\n }\n\n cy.viewport({\n zoom: zoom2,\n pan: pan2,\n cancelOnFailedZoom: true\n });\n distance1 = distance2;\n f1x1 = f1x2;\n f1y1 = f1y2;\n f2x1 = f2x2;\n f2y1 = f2y2;\n r.pinching = true;\n } // Re-project\n\n\n if (e.touches[0]) {\n var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);\n now[0] = pos[0];\n now[1] = pos[1];\n }\n\n if (e.touches[1]) {\n var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY);\n now[2] = pos[0];\n now[3] = pos[1];\n }\n\n if (e.touches[2]) {\n var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY);\n now[4] = pos[0];\n now[5] = pos[1];\n }\n } else if (e.touches[0] && !r.touchData.didSelect // don't allow box selection to degrade to single finger events like panning\n ) {\n var start = r.touchData.start;\n var last = r.touchData.last;\n var near;\n\n if (!r.hoverData.draggingEles && !r.swipePanning) {\n near = r.findNearestElement(now[0], now[1], true, true);\n }\n\n if (capture && start != null) {\n e.preventDefault();\n } // dragging nodes\n\n\n if (capture && start != null && r.nodeIsDraggable(start)) {\n if (isOverThresholdDrag) {\n // then dragging can happen\n var draggedEles = r.dragData.touchDragEles;\n var justStartedDrag = !r.dragData.didDrag;\n\n if (justStartedDrag) {\n addNodesToDrag(draggedEles, {\n inDragLayer: true\n });\n }\n\n r.dragData.didDrag = true;\n var totalShift = {\n x: 0,\n y: 0\n };\n\n if (number(disp[0]) && number(disp[1])) {\n totalShift.x += disp[0];\n totalShift.y += disp[1];\n\n if (justStartedDrag) {\n r.redrawHint('eles', true);\n var dragDelta = r.touchData.dragDelta;\n\n if (dragDelta && number(dragDelta[0]) && number(dragDelta[1])) {\n totalShift.x += dragDelta[0];\n totalShift.y += dragDelta[1];\n }\n }\n }\n\n r.hoverData.draggingEles = true;\n draggedEles.silentShift(totalShift).emit('position drag');\n r.redrawHint('drag', true);\n\n if (r.touchData.startPosition[0] == earlier[0] && r.touchData.startPosition[1] == earlier[1]) {\n r.redrawHint('eles', true);\n }\n\n r.redraw();\n } else {\n // otherise keep track of drag delta for later\n var dragDelta = r.touchData.dragDelta = r.touchData.dragDelta || [];\n\n if (dragDelta.length === 0) {\n dragDelta.push(disp[0]);\n dragDelta.push(disp[1]);\n } else {\n dragDelta[0] += disp[0];\n dragDelta[1] += disp[1];\n }\n }\n } // touchmove\n\n\n {\n triggerEvents(start || near, ['touchmove', 'tapdrag', 'vmousemove'], e, {\n x: now[0],\n y: now[1]\n });\n\n if ((!start || !start.grabbed()) && near != last) {\n if (last) {\n last.emit({\n originalEvent: e,\n type: 'tapdragout',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n\n if (near) {\n near.emit({\n originalEvent: e,\n type: 'tapdragover',\n position: {\n x: now[0],\n y: now[1]\n }\n });\n }\n }\n\n r.touchData.last = near;\n } // check to cancel taphold\n\n if (capture) {\n for (var i = 0; i < now.length; i++) {\n if (now[i] && r.touchData.startPosition[i] && isOverThresholdDrag) {\n r.touchData.singleTouchMoved = true;\n }\n }\n } // panning\n\n\n if (capture && (start == null || start.pannable()) && cy.panningEnabled() && cy.userPanningEnabled()) {\n var allowPassthrough = allowPanningPassthrough(start, r.touchData.starts);\n\n if (allowPassthrough) {\n e.preventDefault();\n\n if (!r.data.bgActivePosistion) {\n r.data.bgActivePosistion = array2point(r.touchData.startPosition);\n }\n\n if (r.swipePanning) {\n cy.panBy({\n x: disp[0] * zoom,\n y: disp[1] * zoom\n });\n } else if (isOverThresholdDrag) {\n r.swipePanning = true;\n cy.panBy({\n x: dx * zoom,\n y: dy * zoom\n });\n\n if (start) {\n start.unactivate();\n r.redrawHint('select', true);\n r.touchData.start = null;\n }\n }\n } // Re-project\n\n\n var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);\n now[0] = pos[0];\n now[1] = pos[1];\n }\n }\n\n for (var j = 0; j < now.length; j++) {\n earlier[j] = now[j];\n } // the active bg indicator should be removed when making a swipe that is neither for dragging nodes or panning\n\n\n if (capture && e.touches.length > 0 && !r.hoverData.draggingEles && !r.swipePanning && r.data.bgActivePosistion != null) {\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n r.redraw();\n }\n }, false);\n var touchcancelHandler;\n r.registerBinding(window, 'touchcancel', touchcancelHandler = function touchcancelHandler(e) {\n // eslint-disable-line no-unused-vars\n var start = r.touchData.start;\n r.touchData.capture = false;\n\n if (start) {\n start.unactivate();\n }\n });\n var touchendHandler;\n r.registerBinding(window, 'touchend', touchendHandler = function touchendHandler(e) {\n // eslint-disable-line no-unused-vars\n var start = r.touchData.start;\n var capture = r.touchData.capture;\n\n if (capture) {\n if (e.touches.length === 0) {\n r.touchData.capture = false;\n }\n\n e.preventDefault();\n } else {\n return;\n }\n\n var select = r.selection;\n r.swipePanning = false;\n r.hoverData.draggingEles = false;\n var cy = r.cy;\n var zoom = cy.zoom();\n var now = r.touchData.now;\n var earlier = r.touchData.earlier;\n\n if (e.touches[0]) {\n var pos = r.projectIntoViewport(e.touches[0].clientX, e.touches[0].clientY);\n now[0] = pos[0];\n now[1] = pos[1];\n }\n\n if (e.touches[1]) {\n var pos = r.projectIntoViewport(e.touches[1].clientX, e.touches[1].clientY);\n now[2] = pos[0];\n now[3] = pos[1];\n }\n\n if (e.touches[2]) {\n var pos = r.projectIntoViewport(e.touches[2].clientX, e.touches[2].clientY);\n now[4] = pos[0];\n now[5] = pos[1];\n }\n\n if (start) {\n start.unactivate();\n }\n\n var ctxTapend;\n\n if (r.touchData.cxt) {\n ctxTapend = {\n originalEvent: e,\n type: 'cxttapend',\n position: {\n x: now[0],\n y: now[1]\n }\n };\n\n if (start) {\n start.emit(ctxTapend);\n } else {\n cy.emit(ctxTapend);\n }\n\n if (!r.touchData.cxtDragged) {\n var ctxTap = {\n originalEvent: e,\n type: 'cxttap',\n position: {\n x: now[0],\n y: now[1]\n }\n };\n\n if (start) {\n start.emit(ctxTap);\n } else {\n cy.emit(ctxTap);\n }\n }\n\n if (r.touchData.start) {\n r.touchData.start._private.grabbed = false;\n }\n\n r.touchData.cxt = false;\n r.touchData.start = null;\n r.redraw();\n return;\n } // no more box selection if we don't have three fingers\n\n\n if (!e.touches[2] && cy.boxSelectionEnabled() && r.touchData.selecting) {\n r.touchData.selecting = false;\n var box = cy.collection(r.getAllInBox(select[0], select[1], select[2], select[3]));\n select[0] = undefined;\n select[1] = undefined;\n select[2] = undefined;\n select[3] = undefined;\n select[4] = 0;\n r.redrawHint('select', true);\n cy.emit({\n type: 'boxend',\n originalEvent: e,\n position: {\n x: now[0],\n y: now[1]\n }\n });\n\n var eleWouldBeSelected = function eleWouldBeSelected(ele) {\n return ele.selectable() && !ele.selected();\n };\n\n box.emit('box').stdFilter(eleWouldBeSelected).select().emit('boxselect');\n\n if (box.nonempty()) {\n r.redrawHint('eles', true);\n }\n\n r.redraw();\n }\n\n if (start != null) {\n start.unactivate();\n }\n\n if (e.touches[2]) {\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n } else if (e.touches[1]) ; else if (e.touches[0]) ; else if (!e.touches[0]) {\n r.data.bgActivePosistion = undefined;\n r.redrawHint('select', true);\n var draggedEles = r.dragData.touchDragEles;\n\n if (start != null) {\n var startWasGrabbed = start._private.grabbed;\n freeDraggedElements(draggedEles);\n r.redrawHint('drag', true);\n r.redrawHint('eles', true);\n\n if (startWasGrabbed) {\n start.emit('freeon');\n draggedEles.emit('free');\n\n if (r.dragData.didDrag) {\n start.emit('dragfreeon');\n draggedEles.emit('dragfree');\n }\n }\n\n triggerEvents(start, ['touchend', 'tapend', 'vmouseup', 'tapdragout'], e, {\n x: now[0],\n y: now[1]\n });\n start.unactivate();\n r.touchData.start = null;\n } else {\n var near = r.findNearestElement(now[0], now[1], true, true);\n triggerEvents(near, ['touchend', 'tapend', 'vmouseup', 'tapdragout'], e, {\n x: now[0],\n y: now[1]\n });\n }\n\n var dx = r.touchData.startPosition[0] - now[0];\n var dx2 = dx * dx;\n var dy = r.touchData.startPosition[1] - now[1];\n var dy2 = dy * dy;\n var dist2 = dx2 + dy2;\n var rdist2 = dist2 * zoom * zoom; // Tap event, roughly same as mouse click event for touch\n\n if (!r.touchData.singleTouchMoved) {\n if (!start) {\n cy.$(':selected').unselect(['tapunselect']);\n }\n\n triggerEvents(start, ['tap', 'vclick'], e, {\n x: now[0],\n y: now[1]\n });\n } // Prepare to select the currently touched node, only if it hasn't been dragged past a certain distance\n\n\n if (start != null && !r.dragData.didDrag // didn't drag nodes around\n && start._private.selectable && rdist2 < r.touchTapThreshold2 && !r.pinching // pinch to zoom should not affect selection\n ) {\n if (cy.selectionType() === 'single') {\n cy.$(isSelected).unmerge(start).unselect(['tapunselect']);\n start.select(['tapselect']);\n } else {\n if (start.selected()) {\n start.unselect(['tapunselect']);\n } else {\n start.select(['tapselect']);\n }\n }\n\n r.redrawHint('eles', true);\n }\n\n r.touchData.singleTouchMoved = true;\n }\n\n for (var j = 0; j < now.length; j++) {\n earlier[j] = now[j];\n }\n\n r.dragData.didDrag = false; // reset for next touchstart\n\n if (e.touches.length === 0) {\n r.touchData.dragDelta = [];\n r.touchData.startPosition = null;\n r.touchData.startGPosition = null;\n r.touchData.didSelect = false;\n }\n\n if (e.touches.length < 2) {\n if (e.touches.length === 1) {\n // the old start global pos'n may not be the same finger that remains\n r.touchData.startGPosition = [e.touches[0].clientX, e.touches[0].clientY];\n }\n\n r.pinching = false;\n r.redrawHint('eles', true);\n r.redraw();\n } //r.redraw();\n\n }, false); // fallback compatibility layer for ms pointer events\n\n if (typeof TouchEvent === 'undefined') {\n var pointers = [];\n\n var makeTouch = function makeTouch(e) {\n return {\n clientX: e.clientX,\n clientY: e.clientY,\n force: 1,\n identifier: e.pointerId,\n pageX: e.pageX,\n pageY: e.pageY,\n radiusX: e.width / 2,\n radiusY: e.height / 2,\n screenX: e.screenX,\n screenY: e.screenY,\n target: e.target\n };\n };\n\n var makePointer = function makePointer(e) {\n return {\n event: e,\n touch: makeTouch(e)\n };\n };\n\n var addPointer = function addPointer(e) {\n pointers.push(makePointer(e));\n };\n\n var removePointer = function removePointer(e) {\n for (var i = 0; i < pointers.length; i++) {\n var p = pointers[i];\n\n if (p.event.pointerId === e.pointerId) {\n pointers.splice(i, 1);\n return;\n }\n }\n };\n\n var updatePointer = function updatePointer(e) {\n var p = pointers.filter(function (p) {\n return p.event.pointerId === e.pointerId;\n })[0];\n p.event = e;\n p.touch = makeTouch(e);\n };\n\n var addTouchesToEvent = function addTouchesToEvent(e) {\n e.touches = pointers.map(function (p) {\n return p.touch;\n });\n };\n\n var pointerIsMouse = function pointerIsMouse(e) {\n return e.pointerType === 'mouse' || e.pointerType === 4;\n };\n\n r.registerBinding(r.container, 'pointerdown', function (e) {\n if (pointerIsMouse(e)) {\n return;\n } // mouse already handled\n\n\n e.preventDefault();\n addPointer(e);\n addTouchesToEvent(e);\n touchstartHandler(e);\n });\n r.registerBinding(r.container, 'pointerup', function (e) {\n if (pointerIsMouse(e)) {\n return;\n } // mouse already handled\n\n\n removePointer(e);\n addTouchesToEvent(e);\n touchendHandler(e);\n });\n r.registerBinding(r.container, 'pointercancel', function (e) {\n if (pointerIsMouse(e)) {\n return;\n } // mouse already handled\n\n\n removePointer(e);\n addTouchesToEvent(e);\n touchcancelHandler(e);\n });\n r.registerBinding(r.container, 'pointermove', function (e) {\n if (pointerIsMouse(e)) {\n return;\n } // mouse already handled\n\n\n e.preventDefault();\n updatePointer(e);\n addTouchesToEvent(e);\n touchmoveHandler(e);\n });\n }\n};\n\nvar BRp$d = {};\n\nBRp$d.generatePolygon = function (name, points) {\n return this.nodeShapes[name] = {\n renderer: this,\n name: name,\n points: points,\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl('polygon', context, centerX, centerY, width, height, this.points);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n return polygonIntersectLine(x, y, this.points, nodeX, nodeY, width / 2, height / 2, padding);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n return pointInsidePolygon(x, y, this.points, centerX, centerY, width, height, [0, -1], padding);\n }\n };\n};\n\nBRp$d.generateEllipse = function () {\n return this.nodeShapes['ellipse'] = {\n renderer: this,\n name: 'ellipse',\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl(this.name, context, centerX, centerY, width, height);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n return intersectLineEllipse(x, y, nodeX, nodeY, width / 2 + padding, height / 2 + padding);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n return checkInEllipse(x, y, width, height, centerX, centerY, padding);\n }\n };\n};\n\nBRp$d.generateRoundPolygon = function (name, points) {\n // Pre-compute control points\n // Since these points depend on the radius length (which in turns depend on the width/height of the node) we will only pre-compute\n // the unit vectors.\n // For simplicity the layout will be:\n // [ p0, UnitVectorP0P1, p1, UniVectorP1P2, ..., pn, UnitVectorPnP0 ]\n var allPoints = new Array(points.length * 2);\n\n for (var i = 0; i < points.length / 2; i++) {\n var sourceIndex = i * 2;\n var destIndex = void 0;\n\n if (i < points.length / 2 - 1) {\n destIndex = (i + 1) * 2;\n } else {\n destIndex = 0;\n }\n\n allPoints[i * 4] = points[sourceIndex];\n allPoints[i * 4 + 1] = points[sourceIndex + 1];\n var xDest = points[destIndex] - points[sourceIndex];\n var yDest = points[destIndex + 1] - points[sourceIndex + 1];\n var norm = Math.sqrt(xDest * xDest + yDest * yDest);\n allPoints[i * 4 + 2] = xDest / norm;\n allPoints[i * 4 + 3] = yDest / norm;\n }\n\n return this.nodeShapes[name] = {\n renderer: this,\n name: name,\n points: allPoints,\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl('round-polygon', context, centerX, centerY, width, height, this.points);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n return roundPolygonIntersectLine(x, y, this.points, nodeX, nodeY, width, height);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n return pointInsideRoundPolygon(x, y, this.points, centerX, centerY, width, height);\n }\n };\n};\n\nBRp$d.generateRoundRectangle = function () {\n return this.nodeShapes['round-rectangle'] = this.nodeShapes['roundrectangle'] = {\n renderer: this,\n name: 'round-rectangle',\n points: generateUnitNgonPointsFitToSquare(4, 0),\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl(this.name, context, centerX, centerY, width, height);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n return roundRectangleIntersectLine(x, y, nodeX, nodeY, width, height, padding);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n var cornerRadius = getRoundRectangleRadius(width, height);\n var diam = cornerRadius * 2; // Check hBox\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width, height - diam, [0, -1], padding)) {\n return true;\n } // Check vBox\n\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width - diam, height, [0, -1], padding)) {\n return true;\n } // Check top left quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX - width / 2 + cornerRadius, centerY - height / 2 + cornerRadius, padding)) {\n return true;\n } // Check top right quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX + width / 2 - cornerRadius, centerY - height / 2 + cornerRadius, padding)) {\n return true;\n } // Check bottom right quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX + width / 2 - cornerRadius, centerY + height / 2 - cornerRadius, padding)) {\n return true;\n } // Check bottom left quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX - width / 2 + cornerRadius, centerY + height / 2 - cornerRadius, padding)) {\n return true;\n }\n\n return false;\n }\n };\n};\n\nBRp$d.generateCutRectangle = function () {\n return this.nodeShapes['cut-rectangle'] = this.nodeShapes['cutrectangle'] = {\n renderer: this,\n name: 'cut-rectangle',\n cornerLength: getCutRectangleCornerLength(),\n points: generateUnitNgonPointsFitToSquare(4, 0),\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl(this.name, context, centerX, centerY, width, height);\n },\n generateCutTrianglePts: function generateCutTrianglePts(width, height, centerX, centerY) {\n var cl = this.cornerLength;\n var hh = height / 2;\n var hw = width / 2;\n var xBegin = centerX - hw;\n var xEnd = centerX + hw;\n var yBegin = centerY - hh;\n var yEnd = centerY + hh; // points are in clockwise order, inner (imaginary) triangle pt on [4, 5]\n\n return {\n topLeft: [xBegin, yBegin + cl, xBegin + cl, yBegin, xBegin + cl, yBegin + cl],\n topRight: [xEnd - cl, yBegin, xEnd, yBegin + cl, xEnd - cl, yBegin + cl],\n bottomRight: [xEnd, yEnd - cl, xEnd - cl, yEnd, xEnd - cl, yEnd - cl],\n bottomLeft: [xBegin + cl, yEnd, xBegin, yEnd - cl, xBegin + cl, yEnd - cl]\n };\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n var cPts = this.generateCutTrianglePts(width + 2 * padding, height + 2 * padding, nodeX, nodeY);\n var pts = [].concat.apply([], [cPts.topLeft.splice(0, 4), cPts.topRight.splice(0, 4), cPts.bottomRight.splice(0, 4), cPts.bottomLeft.splice(0, 4)]);\n return polygonIntersectLine(x, y, pts, nodeX, nodeY);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n // Check hBox\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width, height - 2 * this.cornerLength, [0, -1], padding)) {\n return true;\n } // Check vBox\n\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width - 2 * this.cornerLength, height, [0, -1], padding)) {\n return true;\n }\n\n var cutTrianglePts = this.generateCutTrianglePts(width, height, centerX, centerY);\n return pointInsidePolygonPoints(x, y, cutTrianglePts.topLeft) || pointInsidePolygonPoints(x, y, cutTrianglePts.topRight) || pointInsidePolygonPoints(x, y, cutTrianglePts.bottomRight) || pointInsidePolygonPoints(x, y, cutTrianglePts.bottomLeft);\n }\n };\n};\n\nBRp$d.generateBarrel = function () {\n return this.nodeShapes['barrel'] = {\n renderer: this,\n name: 'barrel',\n points: generateUnitNgonPointsFitToSquare(4, 0),\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl(this.name, context, centerX, centerY, width, height);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n // use two fixed t values for the bezier curve approximation\n var t0 = 0.15;\n var t1 = 0.5;\n var t2 = 0.85;\n var bPts = this.generateBarrelBezierPts(width + 2 * padding, height + 2 * padding, nodeX, nodeY);\n\n var approximateBarrelCurvePts = function approximateBarrelCurvePts(pts) {\n // approximate curve pts based on the two t values\n var m0 = qbezierPtAt({\n x: pts[0],\n y: pts[1]\n }, {\n x: pts[2],\n y: pts[3]\n }, {\n x: pts[4],\n y: pts[5]\n }, t0);\n var m1 = qbezierPtAt({\n x: pts[0],\n y: pts[1]\n }, {\n x: pts[2],\n y: pts[3]\n }, {\n x: pts[4],\n y: pts[5]\n }, t1);\n var m2 = qbezierPtAt({\n x: pts[0],\n y: pts[1]\n }, {\n x: pts[2],\n y: pts[3]\n }, {\n x: pts[4],\n y: pts[5]\n }, t2);\n return [pts[0], pts[1], m0.x, m0.y, m1.x, m1.y, m2.x, m2.y, pts[4], pts[5]];\n };\n\n var pts = [].concat(approximateBarrelCurvePts(bPts.topLeft), approximateBarrelCurvePts(bPts.topRight), approximateBarrelCurvePts(bPts.bottomRight), approximateBarrelCurvePts(bPts.bottomLeft));\n return polygonIntersectLine(x, y, pts, nodeX, nodeY);\n },\n generateBarrelBezierPts: function generateBarrelBezierPts(width, height, centerX, centerY) {\n var hh = height / 2;\n var hw = width / 2;\n var xBegin = centerX - hw;\n var xEnd = centerX + hw;\n var yBegin = centerY - hh;\n var yEnd = centerY + hh;\n var curveConstants = getBarrelCurveConstants(width, height);\n var hOffset = curveConstants.heightOffset;\n var wOffset = curveConstants.widthOffset;\n var ctrlPtXOffset = curveConstants.ctrlPtOffsetPct * width; // points are in clockwise order, inner (imaginary) control pt on [4, 5]\n\n var pts = {\n topLeft: [xBegin, yBegin + hOffset, xBegin + ctrlPtXOffset, yBegin, xBegin + wOffset, yBegin],\n topRight: [xEnd - wOffset, yBegin, xEnd - ctrlPtXOffset, yBegin, xEnd, yBegin + hOffset],\n bottomRight: [xEnd, yEnd - hOffset, xEnd - ctrlPtXOffset, yEnd, xEnd - wOffset, yEnd],\n bottomLeft: [xBegin + wOffset, yEnd, xBegin + ctrlPtXOffset, yEnd, xBegin, yEnd - hOffset]\n };\n pts.topLeft.isTop = true;\n pts.topRight.isTop = true;\n pts.bottomLeft.isBottom = true;\n pts.bottomRight.isBottom = true;\n return pts;\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n var curveConstants = getBarrelCurveConstants(width, height);\n var hOffset = curveConstants.heightOffset;\n var wOffset = curveConstants.widthOffset; // Check hBox\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width, height - 2 * hOffset, [0, -1], padding)) {\n return true;\n } // Check vBox\n\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width - 2 * wOffset, height, [0, -1], padding)) {\n return true;\n }\n\n var barrelCurvePts = this.generateBarrelBezierPts(width, height, centerX, centerY);\n\n var getCurveT = function getCurveT(x, y, curvePts) {\n var x0 = curvePts[4];\n var x1 = curvePts[2];\n var x2 = curvePts[0];\n var y0 = curvePts[5]; // var y1 = curvePts[ 3 ];\n\n var y2 = curvePts[1];\n var xMin = Math.min(x0, x2);\n var xMax = Math.max(x0, x2);\n var yMin = Math.min(y0, y2);\n var yMax = Math.max(y0, y2);\n\n if (xMin <= x && x <= xMax && yMin <= y && y <= yMax) {\n var coeff = bezierPtsToQuadCoeff(x0, x1, x2);\n var roots = solveQuadratic(coeff[0], coeff[1], coeff[2], x);\n var validRoots = roots.filter(function (r) {\n return 0 <= r && r <= 1;\n });\n\n if (validRoots.length > 0) {\n return validRoots[0];\n }\n }\n\n return null;\n };\n\n var curveRegions = Object.keys(barrelCurvePts);\n\n for (var i = 0; i < curveRegions.length; i++) {\n var corner = curveRegions[i];\n var cornerPts = barrelCurvePts[corner];\n var t = getCurveT(x, y, cornerPts);\n\n if (t == null) {\n continue;\n }\n\n var y0 = cornerPts[5];\n var y1 = cornerPts[3];\n var y2 = cornerPts[1];\n var bezY = qbezierAt(y0, y1, y2, t);\n\n if (cornerPts.isTop && bezY <= y) {\n return true;\n }\n\n if (cornerPts.isBottom && y <= bezY) {\n return true;\n }\n }\n\n return false;\n }\n };\n};\n\nBRp$d.generateBottomRoundrectangle = function () {\n return this.nodeShapes['bottom-round-rectangle'] = this.nodeShapes['bottomroundrectangle'] = {\n renderer: this,\n name: 'bottom-round-rectangle',\n points: generateUnitNgonPointsFitToSquare(4, 0),\n draw: function draw(context, centerX, centerY, width, height) {\n this.renderer.nodeShapeImpl(this.name, context, centerX, centerY, width, height);\n },\n intersectLine: function intersectLine(nodeX, nodeY, width, height, x, y, padding) {\n var topStartX = nodeX - (width / 2 + padding);\n var topStartY = nodeY - (height / 2 + padding);\n var topEndY = topStartY;\n var topEndX = nodeX + (width / 2 + padding);\n var topIntersections = finiteLinesIntersect(x, y, nodeX, nodeY, topStartX, topStartY, topEndX, topEndY, false);\n\n if (topIntersections.length > 0) {\n return topIntersections;\n }\n\n return roundRectangleIntersectLine(x, y, nodeX, nodeY, width, height, padding);\n },\n checkPoint: function checkPoint(x, y, padding, width, height, centerX, centerY) {\n var cornerRadius = getRoundRectangleRadius(width, height);\n var diam = 2 * cornerRadius; // Check hBox\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width, height - diam, [0, -1], padding)) {\n return true;\n } // Check vBox\n\n\n if (pointInsidePolygon(x, y, this.points, centerX, centerY, width - diam, height, [0, -1], padding)) {\n return true;\n } // check non-rounded top side\n\n\n var outerWidth = width / 2 + 2 * padding;\n var outerHeight = height / 2 + 2 * padding;\n var points = [centerX - outerWidth, centerY - outerHeight, centerX - outerWidth, centerY, centerX + outerWidth, centerY, centerX + outerWidth, centerY - outerHeight];\n\n if (pointInsidePolygonPoints(x, y, points)) {\n return true;\n } // Check bottom right quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX + width / 2 - cornerRadius, centerY + height / 2 - cornerRadius, padding)) {\n return true;\n } // Check bottom left quarter circle\n\n\n if (checkInEllipse(x, y, diam, diam, centerX - width / 2 + cornerRadius, centerY + height / 2 - cornerRadius, padding)) {\n return true;\n }\n\n return false;\n }\n };\n};\n\nBRp$d.registerNodeShapes = function () {\n var nodeShapes = this.nodeShapes = {};\n var renderer = this;\n this.generateEllipse();\n this.generatePolygon('triangle', generateUnitNgonPointsFitToSquare(3, 0));\n this.generateRoundPolygon('round-triangle', generateUnitNgonPointsFitToSquare(3, 0));\n this.generatePolygon('rectangle', generateUnitNgonPointsFitToSquare(4, 0));\n nodeShapes['square'] = nodeShapes['rectangle'];\n this.generateRoundRectangle();\n this.generateCutRectangle();\n this.generateBarrel();\n this.generateBottomRoundrectangle();\n {\n var diamondPoints = [0, 1, 1, 0, 0, -1, -1, 0];\n this.generatePolygon('diamond', diamondPoints);\n this.generateRoundPolygon('round-diamond', diamondPoints);\n }\n this.generatePolygon('pentagon', generateUnitNgonPointsFitToSquare(5, 0));\n this.generateRoundPolygon('round-pentagon', generateUnitNgonPointsFitToSquare(5, 0));\n this.generatePolygon('hexagon', generateUnitNgonPointsFitToSquare(6, 0));\n this.generateRoundPolygon('round-hexagon', generateUnitNgonPointsFitToSquare(6, 0));\n this.generatePolygon('heptagon', generateUnitNgonPointsFitToSquare(7, 0));\n this.generateRoundPolygon('round-heptagon', generateUnitNgonPointsFitToSquare(7, 0));\n this.generatePolygon('octagon', generateUnitNgonPointsFitToSquare(8, 0));\n this.generateRoundPolygon('round-octagon', generateUnitNgonPointsFitToSquare(8, 0));\n var star5Points = new Array(20);\n {\n var outerPoints = generateUnitNgonPoints(5, 0);\n var innerPoints = generateUnitNgonPoints(5, Math.PI / 5); // Outer radius is 1; inner radius of star is smaller\n\n var innerRadius = 0.5 * (3 - Math.sqrt(5));\n innerRadius *= 1.57;\n\n for (var i = 0; i < innerPoints.length / 2; i++) {\n innerPoints[i * 2] *= innerRadius;\n innerPoints[i * 2 + 1] *= innerRadius;\n }\n\n for (var i = 0; i < 20 / 4; i++) {\n star5Points[i * 4] = outerPoints[i * 2];\n star5Points[i * 4 + 1] = outerPoints[i * 2 + 1];\n star5Points[i * 4 + 2] = innerPoints[i * 2];\n star5Points[i * 4 + 3] = innerPoints[i * 2 + 1];\n }\n }\n star5Points = fitPolygonToSquare(star5Points);\n this.generatePolygon('star', star5Points);\n this.generatePolygon('vee', [-1, -1, 0, -0.333, 1, -1, 0, 1]);\n this.generatePolygon('rhomboid', [-1, -1, 0.333, -1, 1, 1, -0.333, 1]);\n this.nodeShapes['concavehexagon'] = this.generatePolygon('concave-hexagon', [-1, -0.95, -0.75, 0, -1, 0.95, 1, 0.95, 0.75, 0, 1, -0.95]);\n {\n var tagPoints = [-1, -1, 0.25, -1, 1, 0, 0.25, 1, -1, 1];\n this.generatePolygon('tag', tagPoints);\n this.generateRoundPolygon('round-tag', tagPoints);\n }\n\n nodeShapes.makePolygon = function (points) {\n // use caching on user-specified polygons so they are as fast as native shapes\n var key = points.join('$');\n var name = 'polygon-' + key;\n var shape;\n\n if (shape = this[name]) {\n // got cached shape\n return shape;\n } // create and cache new shape\n\n\n return renderer.generatePolygon(name, points);\n };\n};\n\nvar BRp$e = {};\n\nBRp$e.timeToRender = function () {\n return this.redrawTotalTime / this.redrawCount;\n};\n\nBRp$e.redraw = function (options) {\n options = options || staticEmptyObject();\n var r = this;\n\n if (r.averageRedrawTime === undefined) {\n r.averageRedrawTime = 0;\n }\n\n if (r.lastRedrawTime === undefined) {\n r.lastRedrawTime = 0;\n }\n\n if (r.lastDrawTime === undefined) {\n r.lastDrawTime = 0;\n }\n\n r.requestedFrame = true;\n r.renderOptions = options;\n};\n\nBRp$e.beforeRender = function (fn, priority) {\n // the renderer can't add tick callbacks when destroyed\n if (this.destroyed) {\n return;\n }\n\n if (priority == null) {\n error('Priority is not optional for beforeRender');\n }\n\n var cbs = this.beforeRenderCallbacks;\n cbs.push({\n fn: fn,\n priority: priority\n }); // higher priority callbacks executed first\n\n cbs.sort(function (a, b) {\n return b.priority - a.priority;\n });\n};\n\nvar beforeRenderCallbacks = function beforeRenderCallbacks(r, willDraw, startTime) {\n var cbs = r.beforeRenderCallbacks;\n\n for (var i = 0; i < cbs.length; i++) {\n cbs[i].fn(willDraw, startTime);\n }\n};\n\nBRp$e.startRenderLoop = function () {\n var r = this;\n var cy = r.cy;\n\n if (r.renderLoopStarted) {\n return;\n } else {\n r.renderLoopStarted = true;\n }\n\n var renderFn = function renderFn(requestTime) {\n if (r.destroyed) {\n return;\n }\n\n if (cy.batching()) ; else if (r.requestedFrame && !r.skipFrame) {\n beforeRenderCallbacks(r, true, requestTime);\n var startTime = performanceNow();\n r.render(r.renderOptions);\n var endTime = r.lastDrawTime = performanceNow();\n\n if (r.averageRedrawTime === undefined) {\n r.averageRedrawTime = endTime - startTime;\n }\n\n if (r.redrawCount === undefined) {\n r.redrawCount = 0;\n }\n\n r.redrawCount++;\n\n if (r.redrawTotalTime === undefined) {\n r.redrawTotalTime = 0;\n }\n\n var duration = endTime - startTime;\n r.redrawTotalTime += duration;\n r.lastRedrawTime = duration; // use a weighted average with a bias from the previous average so we don't spike so easily\n\n r.averageRedrawTime = r.averageRedrawTime / 2 + duration / 2;\n r.requestedFrame = false;\n } else {\n beforeRenderCallbacks(r, false, requestTime);\n }\n\n r.skipFrame = false;\n requestAnimationFrame(renderFn);\n };\n\n requestAnimationFrame(renderFn);\n};\n\nvar BaseRenderer = function BaseRenderer(options) {\n this.init(options);\n};\n\nvar BR = BaseRenderer;\nvar BRp$f = BR.prototype;\nBRp$f.clientFunctions = ['redrawHint', 'render', 'renderTo', 'matchCanvasSize', 'nodeShapeImpl', 'arrowShapeImpl'];\n\nBRp$f.init = function (options) {\n var r = this;\n r.options = options;\n r.cy = options.cy;\n var ctr = r.container = options.cy.container(); // prepend a stylesheet in the head such that\n\n if (window$1) {\n var document = window$1.document;\n var head = document.head;\n var stylesheetId = '__________cytoscape_stylesheet';\n var className = '__________cytoscape_container';\n var stylesheetAlreadyExists = document.getElementById(stylesheetId) != null;\n\n if (ctr.className.indexOf(className) < 0) {\n ctr.className = (ctr.className || '') + ' ' + className;\n }\n\n if (!stylesheetAlreadyExists) {\n var stylesheet = document.createElement('style');\n stylesheet.id = stylesheetId;\n stylesheet.innerHTML = '.' + className + ' { position: relative; }';\n head.insertBefore(stylesheet, head.children[0]); // first so lowest priority\n }\n\n var computedStyle = window$1.getComputedStyle(ctr);\n var position = computedStyle.getPropertyValue('position');\n\n if (position === 'static') {\n warn('A Cytoscape container has style position:static and so can not use UI extensions properly');\n }\n }\n\n r.selection = [undefined, undefined, undefined, undefined, 0]; // Coordinates for selection box, plus enabled flag\n\n r.bezierProjPcts = [0.05, 0.225, 0.4, 0.5, 0.6, 0.775, 0.95]; //--Pointer-related data\n\n r.hoverData = {\n down: null,\n last: null,\n downTime: null,\n triggerMode: null,\n dragging: false,\n initialPan: [null, null],\n capture: false\n };\n r.dragData = {\n possibleDragElements: []\n };\n r.touchData = {\n start: null,\n capture: false,\n // These 3 fields related to tap, taphold events\n startPosition: [null, null, null, null, null, null],\n singleTouchStartTime: null,\n singleTouchMoved: true,\n now: [null, null, null, null, null, null],\n earlier: [null, null, null, null, null, null]\n };\n r.redraws = 0;\n r.showFps = options.showFps;\n r.debug = options.debug;\n r.hideEdgesOnViewport = options.hideEdgesOnViewport;\n r.textureOnViewport = options.textureOnViewport;\n r.wheelSensitivity = options.wheelSensitivity;\n r.motionBlurEnabled = options.motionBlur; // on by default\n\n r.forcedPixelRatio = number(options.pixelRatio) ? options.pixelRatio : null;\n r.motionBlur = options.motionBlur; // for initial kick off\n\n r.motionBlurOpacity = options.motionBlurOpacity;\n r.motionBlurTransparency = 1 - r.motionBlurOpacity;\n r.motionBlurPxRatio = 1;\n r.mbPxRBlurry = 1; //0.8;\n\n r.minMbLowQualFrames = 4;\n r.fullQualityMb = false;\n r.clearedForMotionBlur = [];\n r.desktopTapThreshold = options.desktopTapThreshold;\n r.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;\n r.touchTapThreshold = options.touchTapThreshold;\n r.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;\n r.tapholdDuration = 500;\n r.bindings = [];\n r.beforeRenderCallbacks = [];\n r.beforeRenderPriorities = {\n // higher priority execs before lower one\n animations: 400,\n eleCalcs: 300,\n eleTxrDeq: 200,\n lyrTxrDeq: 150,\n lyrTxrSkip: 100\n };\n r.registerNodeShapes();\n r.registerArrowShapes();\n r.registerCalculationListeners();\n};\n\nBRp$f.notify = function (eventName, eles) {\n var r = this;\n var cy = r.cy; // the renderer can't be notified after it's destroyed\n\n if (this.destroyed) {\n return;\n }\n\n if (eventName === 'init') {\n r.load();\n return;\n }\n\n if (eventName === 'destroy') {\n r.destroy();\n return;\n }\n\n if (eventName === 'add' || eventName === 'remove' || eventName === 'move' && cy.hasCompoundNodes() || eventName === 'load' || eventName === 'zorder' || eventName === 'mount') {\n r.invalidateCachedZSortedEles();\n }\n\n if (eventName === 'viewport') {\n r.redrawHint('select', true);\n }\n\n if (eventName === 'load' || eventName === 'resize' || eventName === 'mount') {\n r.invalidateContainerClientCoordsCache();\n r.matchCanvasSize(r.container);\n }\n\n r.redrawHint('eles', true);\n r.redrawHint('drag', true);\n this.startRenderLoop();\n this.redraw();\n};\n\nBRp$f.destroy = function () {\n var r = this;\n r.destroyed = true;\n r.cy.stopAnimationLoop();\n\n for (var i = 0; i < r.bindings.length; i++) {\n var binding = r.bindings[i];\n var b = binding;\n var tgt = b.target;\n (tgt.off || tgt.removeEventListener).apply(tgt, b.args);\n }\n\n r.bindings = [];\n r.beforeRenderCallbacks = [];\n r.onUpdateEleCalcsFns = [];\n\n if (r.removeObserver) {\n r.removeObserver.disconnect();\n }\n\n if (r.styleObserver) {\n r.styleObserver.disconnect();\n }\n\n if (r.resizeObserver) {\n r.resizeObserver.disconnect();\n }\n\n if (r.labelCalcDiv) {\n try {\n document.body.removeChild(r.labelCalcDiv); // eslint-disable-line no-undef\n } catch (e) {// ie10 issue #1014\n }\n }\n};\n\nBRp$f.isHeadless = function () {\n return false;\n};\n\n[BRp, BRp$a, BRp$b, BRp$c, BRp$d, BRp$e].forEach(function (props) {\n extend(BRp$f, props);\n});\n\nvar fullFpsTime = 1000 / 60; // assume 60 frames per second\n\nvar defs = {\n setupDequeueing: function setupDequeueing(opts) {\n return function setupDequeueingImpl() {\n var self = this;\n var r = this.renderer;\n\n if (self.dequeueingSetup) {\n return;\n } else {\n self.dequeueingSetup = true;\n }\n\n var queueRedraw = util(function () {\n r.redrawHint('eles', true);\n r.redrawHint('drag', true);\n r.redraw();\n }, opts.deqRedrawThreshold);\n\n var dequeue = function dequeue(willDraw, frameStartTime) {\n var startTime = performanceNow();\n var avgRenderTime = r.averageRedrawTime;\n var renderTime = r.lastRedrawTime;\n var deqd = [];\n var extent = r.cy.extent();\n var pixelRatio = r.getPixelRatio(); // if we aren't in a tick that causes a draw, then the rendered style\n // queue won't automatically be flushed before dequeueing starts\n\n if (!willDraw) {\n r.flushRenderedStyleQueue();\n }\n\n while (true) {\n // eslint-disable-line no-constant-condition\n var now = performanceNow();\n var duration = now - startTime;\n var frameDuration = now - frameStartTime;\n\n if (renderTime < fullFpsTime) {\n // if we're rendering faster than the ideal fps, then do dequeueing\n // during all of the remaining frame time\n var timeAvailable = fullFpsTime - (willDraw ? avgRenderTime : 0);\n\n if (frameDuration >= opts.deqFastCost * timeAvailable) {\n break;\n }\n } else {\n if (willDraw) {\n if (duration >= opts.deqCost * renderTime || duration >= opts.deqAvgCost * avgRenderTime) {\n break;\n }\n } else if (frameDuration >= opts.deqNoDrawCost * fullFpsTime) {\n break;\n }\n }\n\n var thisDeqd = opts.deq(self, pixelRatio, extent);\n\n if (thisDeqd.length > 0) {\n for (var i = 0; i < thisDeqd.length; i++) {\n deqd.push(thisDeqd[i]);\n }\n } else {\n break;\n }\n } // callbacks on dequeue\n\n\n if (deqd.length > 0) {\n opts.onDeqd(self, deqd);\n\n if (!willDraw && opts.shouldRedraw(self, deqd, pixelRatio, extent)) {\n queueRedraw();\n }\n }\n };\n\n var priority = opts.priority || noop;\n r.beforeRender(dequeue, priority(self));\n };\n }\n};\n\n// Uses keys so elements may share the same cache.\n\nvar ElementTextureCacheLookup =\n/*#__PURE__*/\nfunction () {\n function ElementTextureCacheLookup(getKey) {\n var doesEleInvalidateKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : falsify;\n\n _classCallCheck(this, ElementTextureCacheLookup);\n\n this.idsByKey = new Map$1();\n this.keyForId = new Map$1();\n this.cachesByLvl = new Map$1();\n this.lvls = [];\n this.getKey = getKey;\n this.doesEleInvalidateKey = doesEleInvalidateKey;\n }\n\n _createClass(ElementTextureCacheLookup, [{\n key: \"getIdsFor\",\n value: function getIdsFor(key) {\n if (key == null) {\n error(\"Can not get id list for null key\");\n }\n\n var idsByKey = this.idsByKey;\n var ids = this.idsByKey.get(key);\n\n if (!ids) {\n ids = new Set$1();\n idsByKey.set(key, ids);\n }\n\n return ids;\n }\n }, {\n key: \"addIdForKey\",\n value: function addIdForKey(key, id) {\n if (key != null) {\n this.getIdsFor(key).add(id);\n }\n }\n }, {\n key: \"deleteIdForKey\",\n value: function deleteIdForKey(key, id) {\n if (key != null) {\n this.getIdsFor(key)[\"delete\"](id);\n }\n }\n }, {\n key: \"getNumberOfIdsForKey\",\n value: function getNumberOfIdsForKey(key) {\n if (key == null) {\n return 0;\n } else {\n return this.getIdsFor(key).size;\n }\n }\n }, {\n key: \"updateKeyMappingFor\",\n value: function updateKeyMappingFor(ele) {\n var id = ele.id();\n var prevKey = this.keyForId.get(id);\n var currKey = this.getKey(ele);\n this.deleteIdForKey(prevKey, id);\n this.addIdForKey(currKey, id);\n this.keyForId.set(id, currKey);\n }\n }, {\n key: \"deleteKeyMappingFor\",\n value: function deleteKeyMappingFor(ele) {\n var id = ele.id();\n var prevKey = this.keyForId.get(id);\n this.deleteIdForKey(prevKey, id);\n this.keyForId[\"delete\"](id);\n }\n }, {\n key: \"keyHasChangedFor\",\n value: function keyHasChangedFor(ele) {\n var id = ele.id();\n var prevKey = this.keyForId.get(id);\n var newKey = this.getKey(ele);\n return prevKey !== newKey;\n }\n }, {\n key: \"isInvalid\",\n value: function isInvalid(ele) {\n return this.keyHasChangedFor(ele) || this.doesEleInvalidateKey(ele);\n }\n }, {\n key: \"getCachesAt\",\n value: function getCachesAt(lvl) {\n var cachesByLvl = this.cachesByLvl,\n lvls = this.lvls;\n var caches = cachesByLvl.get(lvl);\n\n if (!caches) {\n caches = new Map$1();\n cachesByLvl.set(lvl, caches);\n lvls.push(lvl);\n }\n\n return caches;\n }\n }, {\n key: \"getCache\",\n value: function getCache(key, lvl) {\n return this.getCachesAt(lvl).get(key);\n }\n }, {\n key: \"get\",\n value: function get(ele, lvl) {\n var key = this.getKey(ele);\n var cache = this.getCache(key, lvl); // getting for an element may need to add to the id list b/c eles can share keys\n\n if (cache != null) {\n this.updateKeyMappingFor(ele);\n }\n\n return cache;\n }\n }, {\n key: \"getForCachedKey\",\n value: function getForCachedKey(ele, lvl) {\n var key = this.keyForId.get(ele.id()); // n.b. use cached key, not newly computed key\n\n var cache = this.getCache(key, lvl);\n return cache;\n }\n }, {\n key: \"hasCache\",\n value: function hasCache(key, lvl) {\n return this.getCachesAt(lvl).has(key);\n }\n }, {\n key: \"has\",\n value: function has(ele, lvl) {\n var key = this.getKey(ele);\n return this.hasCache(key, lvl);\n }\n }, {\n key: \"setCache\",\n value: function setCache(key, lvl, cache) {\n cache.key = key;\n this.getCachesAt(lvl).set(key, cache);\n }\n }, {\n key: \"set\",\n value: function set(ele, lvl, cache) {\n var key = this.getKey(ele);\n this.setCache(key, lvl, cache);\n this.updateKeyMappingFor(ele);\n }\n }, {\n key: \"deleteCache\",\n value: function deleteCache(key, lvl) {\n this.getCachesAt(lvl)[\"delete\"](key);\n }\n }, {\n key: \"delete\",\n value: function _delete(ele, lvl) {\n var key = this.getKey(ele);\n this.deleteCache(key, lvl);\n }\n }, {\n key: \"invalidateKey\",\n value: function invalidateKey(key) {\n var _this = this;\n\n this.lvls.forEach(function (lvl) {\n return _this.deleteCache(key, lvl);\n });\n } // returns true if no other eles reference the invalidated cache (n.b. other eles may need the cache with the same key)\n\n }, {\n key: \"invalidate\",\n value: function invalidate(ele) {\n var id = ele.id();\n var key = this.keyForId.get(id); // n.b. use stored key rather than current (potential key)\n\n this.deleteKeyMappingFor(ele);\n var entireKeyInvalidated = this.doesEleInvalidateKey(ele);\n\n if (entireKeyInvalidated) {\n // clear mapping for current key\n this.invalidateKey(key);\n }\n\n return entireKeyInvalidated || this.getNumberOfIdsForKey(key) === 0;\n }\n }]);\n\n return ElementTextureCacheLookup;\n}();\n\nvar minTxrH = 25; // the size of the texture cache for small height eles (special case)\n\nvar txrStepH = 50; // the min size of the regular cache, and the size it increases with each step up\n\nvar minLvl = -4; // when scaling smaller than that we don't need to re-render\n\nvar maxLvl = 3; // when larger than this scale just render directly (caching is not helpful)\n\nvar maxZoom = 7.99; // beyond this zoom level, layered textures are not used\n\nvar eleTxrSpacing = 8; // spacing between elements on textures to avoid blitting overlaps\n\nvar defTxrWidth = 1024; // default/minimum texture width\n\nvar maxTxrW = 1024; // the maximum width of a texture\n\nvar maxTxrH = 1024; // the maximum height of a texture\n\nvar minUtility = 0.2; // if usage of texture is less than this, it is retired\n\nvar maxFullness = 0.8; // fullness of texture after which queue removal is checked\n\nvar maxFullnessChecks = 10; // dequeued after this many checks\n\nvar deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame\n\nvar deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time\n\nvar deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing\n\nvar deqFastCost = 0.9; // % of frame time to be used when >60fps\n\nvar deqRedrawThreshold = 100; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile\n\nvar maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch\n\nvar getTxrReasons = {\n dequeue: 'dequeue',\n downscale: 'downscale',\n highQuality: 'highQuality'\n};\nvar initDefaults = defaults({\n getKey: null,\n doesEleInvalidateKey: falsify,\n drawElement: null,\n getBoundingBox: null,\n getRotationPoint: null,\n getRotationOffset: null,\n isVisible: trueify,\n allowEdgeTxrCaching: true,\n allowParentTxrCaching: true\n});\n\nvar ElementTextureCache = function ElementTextureCache(renderer, initOptions) {\n var self = this;\n self.renderer = renderer;\n self.onDequeues = [];\n var opts = initDefaults(initOptions);\n extend(self, opts);\n self.lookup = new ElementTextureCacheLookup(opts.getKey, opts.doesEleInvalidateKey);\n self.setupDequeueing();\n};\n\nvar ETCp = ElementTextureCache.prototype;\nETCp.reasons = getTxrReasons; // the list of textures in which new subtextures for elements can be placed\n\nETCp.getTextureQueue = function (txrH) {\n var self = this;\n self.eleImgCaches = self.eleImgCaches || {};\n return self.eleImgCaches[txrH] = self.eleImgCaches[txrH] || [];\n}; // the list of usused textures which can be recycled (in use in texture queue)\n\n\nETCp.getRetiredTextureQueue = function (txrH) {\n var self = this;\n var rtxtrQs = self.eleImgCaches.retired = self.eleImgCaches.retired || {};\n var rtxtrQ = rtxtrQs[txrH] = rtxtrQs[txrH] || [];\n return rtxtrQ;\n}; // queue of element draw requests at different scale levels\n\n\nETCp.getElementQueue = function () {\n var self = this;\n var q = self.eleCacheQueue = self.eleCacheQueue || new Heap(function (a, b) {\n return b.reqs - a.reqs;\n });\n return q;\n}; // queue of element draw requests at different scale levels (element id lookup)\n\n\nETCp.getElementKeyToQueue = function () {\n var self = this;\n var k2q = self.eleKeyToCacheQueue = self.eleKeyToCacheQueue || {};\n return k2q;\n};\n\nETCp.getElement = function (ele, bb, pxRatio, lvl, reason) {\n var self = this;\n var r = this.renderer;\n var zoom = r.cy.zoom();\n var lookup = this.lookup;\n\n if (!bb || bb.w === 0 || bb.h === 0 || isNaN(bb.w) || isNaN(bb.h) || !ele.visible() || ele.removed()) {\n return null;\n }\n\n if (!self.allowEdgeTxrCaching && ele.isEdge() || !self.allowParentTxrCaching && ele.isParent()) {\n return null;\n }\n\n if (lvl == null) {\n lvl = Math.ceil(log2(zoom * pxRatio));\n }\n\n if (lvl < minLvl) {\n lvl = minLvl;\n } else if (zoom >= maxZoom || lvl > maxLvl) {\n return null;\n }\n\n var scale = Math.pow(2, lvl);\n var eleScaledH = bb.h * scale;\n var eleScaledW = bb.w * scale;\n var scaledLabelShown = r.eleTextBiggerThanMin(ele, scale);\n\n if (!this.isVisible(ele, scaledLabelShown)) {\n return null;\n }\n\n var eleCache = lookup.get(ele, lvl); // if this get was on an unused/invalidated cache, then restore the texture usage metric\n\n if (eleCache && eleCache.invalidated) {\n eleCache.invalidated = false;\n eleCache.texture.invalidatedWidth -= eleCache.width;\n }\n\n if (eleCache) {\n return eleCache;\n }\n\n var txrH; // which texture height this ele belongs to\n\n if (eleScaledH <= minTxrH) {\n txrH = minTxrH;\n } else if (eleScaledH <= txrStepH) {\n txrH = txrStepH;\n } else {\n txrH = Math.ceil(eleScaledH / txrStepH) * txrStepH;\n }\n\n if (eleScaledH > maxTxrH || eleScaledW > maxTxrW) {\n return null; // caching large elements is not efficient\n }\n\n var txrQ = self.getTextureQueue(txrH); // first try the second last one in case it has space at the end\n\n var txr = txrQ[txrQ.length - 2];\n\n var addNewTxr = function addNewTxr() {\n return self.recycleTexture(txrH, eleScaledW) || self.addTexture(txrH, eleScaledW);\n }; // try the last one if there is no second last one\n\n\n if (!txr) {\n txr = txrQ[txrQ.length - 1];\n } // if the last one doesn't exist, we need a first one\n\n\n if (!txr) {\n txr = addNewTxr();\n } // if there's no room in the current texture, we need a new one\n\n\n if (txr.width - txr.usedWidth < eleScaledW) {\n txr = addNewTxr();\n }\n\n var scalableFrom = function scalableFrom(otherCache) {\n return otherCache && otherCache.scaledLabelShown === scaledLabelShown;\n };\n\n var deqing = reason && reason === getTxrReasons.dequeue;\n var highQualityReq = reason && reason === getTxrReasons.highQuality;\n var downscaleReq = reason && reason === getTxrReasons.downscale;\n var higherCache; // the nearest cache with a higher level\n\n for (var l = lvl + 1; l <= maxLvl; l++) {\n var c = lookup.get(ele, l);\n\n if (c) {\n higherCache = c;\n break;\n }\n }\n\n var oneUpCache = higherCache && higherCache.level === lvl + 1 ? higherCache : null;\n\n var downscale = function downscale() {\n txr.context.drawImage(oneUpCache.texture.canvas, oneUpCache.x, 0, oneUpCache.width, oneUpCache.height, txr.usedWidth, 0, eleScaledW, eleScaledH);\n }; // reset ele area in texture\n\n\n txr.context.setTransform(1, 0, 0, 1, 0, 0);\n txr.context.clearRect(txr.usedWidth, 0, eleScaledW, txrH);\n\n if (scalableFrom(oneUpCache)) {\n // then we can relatively cheaply rescale the existing image w/o rerendering\n downscale();\n } else if (scalableFrom(higherCache)) {\n // then use the higher cache for now and queue the next level down\n // to cheaply scale towards the smaller level\n if (highQualityReq) {\n for (var _l = higherCache.level; _l > lvl; _l--) {\n oneUpCache = self.getElement(ele, bb, pxRatio, _l, getTxrReasons.downscale);\n }\n\n downscale();\n } else {\n self.queueElement(ele, higherCache.level - 1);\n return higherCache;\n }\n } else {\n var lowerCache; // the nearest cache with a lower level\n\n if (!deqing && !highQualityReq && !downscaleReq) {\n for (var _l2 = lvl - 1; _l2 >= minLvl; _l2--) {\n var _c = lookup.get(ele, _l2);\n\n if (_c) {\n lowerCache = _c;\n break;\n }\n }\n }\n\n if (scalableFrom(lowerCache)) {\n // then use the lower quality cache for now and queue the better one for later\n self.queueElement(ele, lvl);\n return lowerCache;\n }\n\n txr.context.translate(txr.usedWidth, 0);\n txr.context.scale(scale, scale);\n this.drawElement(txr.context, ele, bb, scaledLabelShown, false);\n txr.context.scale(1 / scale, 1 / scale);\n txr.context.translate(-txr.usedWidth, 0);\n }\n\n eleCache = {\n x: txr.usedWidth,\n texture: txr,\n level: lvl,\n scale: scale,\n width: eleScaledW,\n height: eleScaledH,\n scaledLabelShown: scaledLabelShown\n };\n txr.usedWidth += Math.ceil(eleScaledW + eleTxrSpacing);\n txr.eleCaches.push(eleCache);\n lookup.set(ele, lvl, eleCache);\n self.checkTextureFullness(txr);\n return eleCache;\n};\n\nETCp.invalidateElements = function (eles) {\n for (var i = 0; i < eles.length; i++) {\n this.invalidateElement(eles[i]);\n }\n};\n\nETCp.invalidateElement = function (ele) {\n var self = this;\n var lookup = self.lookup;\n var caches = [];\n var invalid = lookup.isInvalid(ele);\n\n if (!invalid) {\n return; // override the invalidation request if the element key has not changed\n }\n\n for (var lvl = minLvl; lvl <= maxLvl; lvl++) {\n var cache = lookup.getForCachedKey(ele, lvl);\n\n if (cache) {\n caches.push(cache);\n }\n }\n\n var noOtherElesUseCache = lookup.invalidate(ele);\n\n if (noOtherElesUseCache) {\n for (var i = 0; i < caches.length; i++) {\n var _cache = caches[i];\n var txr = _cache.texture; // remove space from the texture it belongs to\n\n txr.invalidatedWidth += _cache.width; // mark the cache as invalidated\n\n _cache.invalidated = true; // retire the texture if its utility is low\n\n self.checkTextureUtility(txr);\n }\n } // remove from queue since the old req was for the old state\n\n\n self.removeFromQueue(ele);\n};\n\nETCp.checkTextureUtility = function (txr) {\n // invalidate all entries in the cache if the cache size is small\n if (txr.invalidatedWidth >= minUtility * txr.width) {\n this.retireTexture(txr);\n }\n};\n\nETCp.checkTextureFullness = function (txr) {\n // if texture has been mostly filled and passed over several times, remove\n // it from the queue so we don't need to waste time looking at it to put new things\n var self = this;\n var txrQ = self.getTextureQueue(txr.height);\n\n if (txr.usedWidth / txr.width > maxFullness && txr.fullnessChecks >= maxFullnessChecks) {\n removeFromArray(txrQ, txr);\n } else {\n txr.fullnessChecks++;\n }\n};\n\nETCp.retireTexture = function (txr) {\n var self = this;\n var txrH = txr.height;\n var txrQ = self.getTextureQueue(txrH);\n var lookup = this.lookup; // retire the texture from the active / searchable queue:\n\n removeFromArray(txrQ, txr);\n txr.retired = true; // remove the refs from the eles to the caches:\n\n var eleCaches = txr.eleCaches;\n\n for (var i = 0; i < eleCaches.length; i++) {\n var eleCache = eleCaches[i];\n lookup.deleteCache(eleCache.key, eleCache.level);\n }\n\n clearArray(eleCaches); // add the texture to a retired queue so it can be recycled in future:\n\n var rtxtrQ = self.getRetiredTextureQueue(txrH);\n rtxtrQ.push(txr);\n};\n\nETCp.addTexture = function (txrH, minW) {\n var self = this;\n var txrQ = self.getTextureQueue(txrH);\n var txr = {};\n txrQ.push(txr);\n txr.eleCaches = [];\n txr.height = txrH;\n txr.width = Math.max(defTxrWidth, minW);\n txr.usedWidth = 0;\n txr.invalidatedWidth = 0;\n txr.fullnessChecks = 0;\n txr.canvas = self.renderer.makeOffscreenCanvas(txr.width, txr.height);\n txr.context = txr.canvas.getContext('2d');\n return txr;\n};\n\nETCp.recycleTexture = function (txrH, minW) {\n var self = this;\n var txrQ = self.getTextureQueue(txrH);\n var rtxtrQ = self.getRetiredTextureQueue(txrH);\n\n for (var i = 0; i < rtxtrQ.length; i++) {\n var txr = rtxtrQ[i];\n\n if (txr.width >= minW) {\n txr.retired = false;\n txr.usedWidth = 0;\n txr.invalidatedWidth = 0;\n txr.fullnessChecks = 0;\n clearArray(txr.eleCaches);\n txr.context.setTransform(1, 0, 0, 1, 0, 0);\n txr.context.clearRect(0, 0, txr.width, txr.height);\n removeFromArray(rtxtrQ, txr);\n txrQ.push(txr);\n return txr;\n }\n }\n};\n\nETCp.queueElement = function (ele, lvl) {\n var self = this;\n var q = self.getElementQueue();\n var k2q = self.getElementKeyToQueue();\n var key = this.getKey(ele);\n var existingReq = k2q[key];\n\n if (existingReq) {\n // use the max lvl b/c in between lvls are cheap to make\n existingReq.level = Math.max(existingReq.level, lvl);\n existingReq.eles.merge(ele);\n existingReq.reqs++;\n q.updateItem(existingReq);\n } else {\n var req = {\n eles: ele.spawn().merge(ele),\n level: lvl,\n reqs: 1,\n key: key\n };\n q.push(req);\n k2q[key] = req;\n }\n};\n\nETCp.dequeue = function (pxRatio\n/*, extent*/\n) {\n var self = this;\n var q = self.getElementQueue();\n var k2q = self.getElementKeyToQueue();\n var dequeued = [];\n var lookup = self.lookup;\n\n for (var i = 0; i < maxDeqSize; i++) {\n if (q.size() > 0) {\n var req = q.pop();\n var key = req.key;\n var ele = req.eles[0]; // all eles have the same key\n\n var cacheExists = lookup.hasCache(ele, req.level); // clear out the key to req lookup\n\n k2q[key] = null; // dequeueing isn't necessary with an existing cache\n\n if (cacheExists) {\n continue;\n }\n\n dequeued.push(req);\n var bb = self.getBoundingBox(ele);\n self.getElement(ele, bb, pxRatio, req.level, getTxrReasons.dequeue);\n } else {\n break;\n }\n }\n\n return dequeued;\n};\n\nETCp.removeFromQueue = function (ele) {\n var self = this;\n var q = self.getElementQueue();\n var k2q = self.getElementKeyToQueue();\n var key = this.getKey(ele);\n var req = k2q[key];\n\n if (req != null) {\n if (req.eles.length === 1) {\n // remove if last ele in the req\n // bring to front of queue\n req.reqs = MAX_INT;\n q.updateItem(req);\n q.pop(); // remove from queue\n\n k2q[key] = null; // remove from lookup map\n } else {\n // otherwise just remove ele from req\n req.eles.unmerge(ele);\n }\n }\n};\n\nETCp.onDequeue = function (fn) {\n this.onDequeues.push(fn);\n};\n\nETCp.offDequeue = function (fn) {\n removeFromArray(this.onDequeues, fn);\n};\n\nETCp.setupDequeueing = defs.setupDequeueing({\n deqRedrawThreshold: deqRedrawThreshold,\n deqCost: deqCost,\n deqAvgCost: deqAvgCost,\n deqNoDrawCost: deqNoDrawCost,\n deqFastCost: deqFastCost,\n deq: function deq(self, pxRatio, extent) {\n return self.dequeue(pxRatio, extent);\n },\n onDeqd: function onDeqd(self, deqd) {\n for (var i = 0; i < self.onDequeues.length; i++) {\n var fn = self.onDequeues[i];\n fn(deqd);\n }\n },\n shouldRedraw: function shouldRedraw(self, deqd, pxRatio, extent) {\n for (var i = 0; i < deqd.length; i++) {\n var eles = deqd[i].eles;\n\n for (var j = 0; j < eles.length; j++) {\n var bb = eles[j].boundingBox();\n\n if (boundingBoxesIntersect(bb, extent)) {\n return true;\n }\n }\n }\n\n return false;\n },\n priority: function priority(self) {\n return self.renderer.beforeRenderPriorities.eleTxrDeq;\n }\n});\n\nvar defNumLayers = 1; // default number of layers to use\n\nvar minLvl$1 = -4; // when scaling smaller than that we don't need to re-render\n\nvar maxLvl$1 = 2; // when larger than this scale just render directly (caching is not helpful)\n\nvar maxZoom$1 = 3.99; // beyond this zoom level, layered textures are not used\n\nvar deqRedrawThreshold$1 = 50; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile\n\nvar refineEleDebounceTime = 50; // time to debounce sharper ele texture updates\n\nvar deqCost$1 = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame\n\nvar deqAvgCost$1 = 0.1; // % of add'l rendering cost compared to average overall redraw time\n\nvar deqNoDrawCost$1 = 0.9; // % of avg frame time that can be used for dequeueing when not drawing\n\nvar deqFastCost$1 = 0.9; // % of frame time to be used when >60fps\n\nvar maxDeqSize$1 = 1; // number of eles to dequeue and render at higher texture in each batch\n\nvar invalidThreshold = 250; // time threshold for disabling b/c of invalidations\n\nvar maxLayerArea = 4000 * 4000; // layers can't be bigger than this\n\nvar useHighQualityEleTxrReqs = true; // whether to use high quality ele txr requests (generally faster and cheaper in the longterm)\n// var log = function(){ console.log.apply( console, arguments ); };\n\nvar LayeredTextureCache = function LayeredTextureCache(renderer) {\n var self = this;\n var r = self.renderer = renderer;\n var cy = r.cy;\n self.layersByLevel = {}; // e.g. 2 => [ layer1, layer2, ..., layerN ]\n\n self.firstGet = true;\n self.lastInvalidationTime = performanceNow() - 2 * invalidThreshold;\n self.skipping = false;\n self.eleTxrDeqs = cy.collection();\n self.scheduleElementRefinement = util(function () {\n self.refineElementTextures(self.eleTxrDeqs);\n self.eleTxrDeqs.unmerge(self.eleTxrDeqs);\n }, refineEleDebounceTime);\n r.beforeRender(function (willDraw, now) {\n if (now - self.lastInvalidationTime <= invalidThreshold) {\n self.skipping = true;\n } else {\n self.skipping = false;\n }\n }, r.beforeRenderPriorities.lyrTxrSkip);\n\n var qSort = function qSort(a, b) {\n return b.reqs - a.reqs;\n };\n\n self.layersQueue = new Heap(qSort);\n self.setupDequeueing();\n};\n\nvar LTCp = LayeredTextureCache.prototype;\nvar layerIdPool = 0;\nvar MAX_INT$1 = Math.pow(2, 53) - 1;\n\nLTCp.makeLayer = function (bb, lvl) {\n var scale = Math.pow(2, lvl);\n var w = Math.ceil(bb.w * scale);\n var h = Math.ceil(bb.h * scale);\n var canvas = this.renderer.makeOffscreenCanvas(w, h);\n var layer = {\n id: layerIdPool = ++layerIdPool % MAX_INT$1,\n bb: bb,\n level: lvl,\n width: w,\n height: h,\n canvas: canvas,\n context: canvas.getContext('2d'),\n eles: [],\n elesQueue: [],\n reqs: 0\n }; // log('make layer %s with w %s and h %s and lvl %s', layer.id, layer.width, layer.height, layer.level);\n\n var cxt = layer.context;\n var dx = -layer.bb.x1;\n var dy = -layer.bb.y1; // do the transform on creation to save cycles (it's the same for all eles)\n\n cxt.scale(scale, scale);\n cxt.translate(dx, dy);\n return layer;\n};\n\nLTCp.getLayers = function (eles, pxRatio, lvl) {\n var self = this;\n var r = self.renderer;\n var cy = r.cy;\n var zoom = cy.zoom();\n var firstGet = self.firstGet;\n self.firstGet = false; // log('--\\nget layers with %s eles', eles.length);\n //log eles.map(function(ele){ return ele.id() }) );\n\n if (lvl == null) {\n lvl = Math.ceil(log2(zoom * pxRatio));\n\n if (lvl < minLvl$1) {\n lvl = minLvl$1;\n } else if (zoom >= maxZoom$1 || lvl > maxLvl$1) {\n return null;\n }\n }\n\n self.validateLayersElesOrdering(lvl, eles);\n var layersByLvl = self.layersByLevel;\n var scale = Math.pow(2, lvl);\n var layers = layersByLvl[lvl] = layersByLvl[lvl] || [];\n var bb;\n var lvlComplete = self.levelIsComplete(lvl, eles);\n var tmpLayers;\n\n var checkTempLevels = function checkTempLevels() {\n var canUseAsTmpLvl = function canUseAsTmpLvl(l) {\n self.validateLayersElesOrdering(l, eles);\n\n if (self.levelIsComplete(l, eles)) {\n tmpLayers = layersByLvl[l];\n return true;\n }\n };\n\n var checkLvls = function checkLvls(dir) {\n if (tmpLayers) {\n return;\n }\n\n for (var l = lvl + dir; minLvl$1 <= l && l <= maxLvl$1; l += dir) {\n if (canUseAsTmpLvl(l)) {\n break;\n }\n }\n };\n\n checkLvls(+1);\n checkLvls(-1); // remove the invalid layers; they will be replaced as needed later in this function\n\n for (var i = layers.length - 1; i >= 0; i--) {\n var layer = layers[i];\n\n if (layer.invalid) {\n removeFromArray(layers, layer);\n }\n }\n };\n\n if (!lvlComplete) {\n // if the current level is incomplete, then use the closest, best quality layerset temporarily\n // and later queue the current layerset so we can get the proper quality level soon\n checkTempLevels();\n } else {\n // log('level complete, using existing layers\\n--');\n return layers;\n }\n\n var getBb = function getBb() {\n if (!bb) {\n bb = makeBoundingBox();\n\n for (var i = 0; i < eles.length; i++) {\n updateBoundingBox(bb, eles[i].boundingBox());\n }\n }\n\n return bb;\n };\n\n var makeLayer = function makeLayer(opts) {\n opts = opts || {};\n var after = opts.after;\n getBb();\n var area = bb.w * scale * (bb.h * scale);\n\n if (area > maxLayerArea) {\n return null;\n }\n\n var layer = self.makeLayer(bb, lvl);\n\n if (after != null) {\n var index = layers.indexOf(after) + 1;\n layers.splice(index, 0, layer);\n } else if (opts.insert === undefined || opts.insert) {\n // no after specified => first layer made so put at start\n layers.unshift(layer);\n } // if( tmpLayers ){\n //self.queueLayer( layer );\n // }\n\n\n return layer;\n };\n\n if (self.skipping && !firstGet) {\n // log('skip layers');\n return null;\n } // log('do layers');\n\n\n var layer = null;\n var maxElesPerLayer = eles.length / defNumLayers;\n var allowLazyQueueing = !firstGet;\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n var rs = ele._private.rscratch;\n var caches = rs.imgLayerCaches = rs.imgLayerCaches || {}; // log('look at ele', ele.id());\n\n var existingLayer = caches[lvl];\n\n if (existingLayer) {\n // reuse layer for later eles\n // log('reuse layer for', ele.id());\n layer = existingLayer;\n continue;\n }\n\n if (!layer || layer.eles.length >= maxElesPerLayer || !boundingBoxInBoundingBox(layer.bb, ele.boundingBox())) {\n // log('make new layer for ele %s', ele.id());\n layer = makeLayer({\n insert: true,\n after: layer\n }); // if now layer can be built then we can't use layers at this level\n\n if (!layer) {\n return null;\n } // log('new layer with id %s', layer.id);\n\n }\n\n if (tmpLayers || allowLazyQueueing) {\n // log('queue ele %s in layer %s', ele.id(), layer.id);\n self.queueLayer(layer, ele);\n } else {\n // log('draw ele %s in layer %s', ele.id(), layer.id);\n self.drawEleInLayer(layer, ele, lvl, pxRatio);\n }\n\n layer.eles.push(ele);\n caches[lvl] = layer;\n } // log('--');\n\n\n if (tmpLayers) {\n // then we only queued the current layerset and can't draw it yet\n return tmpLayers;\n }\n\n if (allowLazyQueueing) {\n // log('lazy queue level', lvl);\n return null;\n }\n\n return layers;\n}; // a layer may want to use an ele cache of a higher level to avoid blurriness\n// so the layer level might not equal the ele level\n\n\nLTCp.getEleLevelForLayerLevel = function (lvl, pxRatio) {\n return lvl;\n};\n\nLTCp.drawEleInLayer = function (layer, ele, lvl, pxRatio) {\n var self = this;\n var r = this.renderer;\n var context = layer.context;\n var bb = ele.boundingBox();\n\n if (bb.w === 0 || bb.h === 0 || !ele.visible()) {\n return;\n }\n\n lvl = self.getEleLevelForLayerLevel(lvl, pxRatio);\n\n {\n r.setImgSmoothing(context, false);\n }\n\n {\n r.drawCachedElement(context, ele, null, null, lvl, useHighQualityEleTxrReqs);\n }\n\n {\n r.setImgSmoothing(context, true);\n }\n};\n\nLTCp.levelIsComplete = function (lvl, eles) {\n var self = this;\n var layers = self.layersByLevel[lvl];\n\n if (!layers || layers.length === 0) {\n return false;\n }\n\n var numElesInLayers = 0;\n\n for (var i = 0; i < layers.length; i++) {\n var layer = layers[i]; // if there are any eles needed to be drawn yet, the level is not complete\n\n if (layer.reqs > 0) {\n return false;\n } // if the layer is invalid, the level is not complete\n\n\n if (layer.invalid) {\n return false;\n }\n\n numElesInLayers += layer.eles.length;\n } // we should have exactly the number of eles passed in to be complete\n\n\n if (numElesInLayers !== eles.length) {\n return false;\n }\n\n return true;\n};\n\nLTCp.validateLayersElesOrdering = function (lvl, eles) {\n var layers = this.layersByLevel[lvl];\n\n if (!layers) {\n return;\n } // if in a layer the eles are not in the same order, then the layer is invalid\n // (i.e. there is an ele in between the eles in the layer)\n\n\n for (var i = 0; i < layers.length; i++) {\n var layer = layers[i];\n var offset = -1; // find the offset\n\n for (var j = 0; j < eles.length; j++) {\n if (layer.eles[0] === eles[j]) {\n offset = j;\n break;\n }\n }\n\n if (offset < 0) {\n // then the layer has nonexistant elements and is invalid\n this.invalidateLayer(layer);\n continue;\n } // the eles in the layer must be in the same continuous order, else the layer is invalid\n\n\n var o = offset;\n\n for (var j = 0; j < layer.eles.length; j++) {\n if (layer.eles[j] !== eles[o + j]) {\n // log('invalidate based on ordering', layer.id);\n this.invalidateLayer(layer);\n break;\n }\n }\n }\n};\n\nLTCp.updateElementsInLayers = function (eles, update) {\n var self = this;\n var isEles = element(eles[0]); // collect udpated elements (cascaded from the layers) and update each\n // layer itself along the way\n\n for (var i = 0; i < eles.length; i++) {\n var req = isEles ? null : eles[i];\n var ele = isEles ? eles[i] : eles[i].ele;\n var rs = ele._private.rscratch;\n var caches = rs.imgLayerCaches = rs.imgLayerCaches || {};\n\n for (var l = minLvl$1; l <= maxLvl$1; l++) {\n var layer = caches[l];\n\n if (!layer) {\n continue;\n } // if update is a request from the ele cache, then it affects only\n // the matching level\n\n\n if (req && self.getEleLevelForLayerLevel(layer.level) !== req.level) {\n continue;\n }\n\n update(layer, ele, req);\n }\n }\n};\n\nLTCp.haveLayers = function () {\n var self = this;\n var haveLayers = false;\n\n for (var l = minLvl$1; l <= maxLvl$1; l++) {\n var layers = self.layersByLevel[l];\n\n if (layers && layers.length > 0) {\n haveLayers = true;\n break;\n }\n }\n\n return haveLayers;\n};\n\nLTCp.invalidateElements = function (eles) {\n var self = this;\n\n if (eles.length === 0) {\n return;\n }\n\n self.lastInvalidationTime = performanceNow(); // log('update invalidate layer time from eles');\n\n if (eles.length === 0 || !self.haveLayers()) {\n return;\n }\n\n self.updateElementsInLayers(eles, function invalAssocLayers(layer, ele, req) {\n self.invalidateLayer(layer);\n });\n};\n\nLTCp.invalidateLayer = function (layer) {\n // log('update invalidate layer time');\n this.lastInvalidationTime = performanceNow();\n\n if (layer.invalid) {\n return;\n } // save cycles\n\n\n var lvl = layer.level;\n var eles = layer.eles;\n var layers = this.layersByLevel[lvl]; // log('invalidate layer', layer.id );\n\n removeFromArray(layers, layer); // layer.eles = [];\n\n layer.elesQueue = [];\n layer.invalid = true;\n\n if (layer.replacement) {\n layer.replacement.invalid = true;\n }\n\n for (var i = 0; i < eles.length; i++) {\n var caches = eles[i]._private.rscratch.imgLayerCaches;\n\n if (caches) {\n caches[lvl] = null;\n }\n }\n};\n\nLTCp.refineElementTextures = function (eles) {\n var self = this; // log('refine', eles.length);\n\n self.updateElementsInLayers(eles, function refineEachEle(layer, ele, req) {\n var rLyr = layer.replacement;\n\n if (!rLyr) {\n rLyr = layer.replacement = self.makeLayer(layer.bb, layer.level);\n rLyr.replaces = layer;\n rLyr.eles = layer.eles; // log('make replacement layer %s for %s with level %s', rLyr.id, layer.id, rLyr.level);\n }\n\n if (!rLyr.reqs) {\n for (var i = 0; i < rLyr.eles.length; i++) {\n self.queueLayer(rLyr, rLyr.eles[i]);\n } // log('queue replacement layer refinement', rLyr.id);\n\n }\n });\n};\n\nLTCp.enqueueElementRefinement = function (ele) {\n\n this.eleTxrDeqs.merge(ele);\n this.scheduleElementRefinement();\n};\n\nLTCp.queueLayer = function (layer, ele) {\n var self = this;\n var q = self.layersQueue;\n var elesQ = layer.elesQueue;\n var hasId = elesQ.hasId = elesQ.hasId || {}; // if a layer is going to be replaced, queuing is a waste of time\n\n if (layer.replacement) {\n return;\n }\n\n if (ele) {\n if (hasId[ele.id()]) {\n return;\n }\n\n elesQ.push(ele);\n hasId[ele.id()] = true;\n }\n\n if (layer.reqs) {\n layer.reqs++;\n q.updateItem(layer);\n } else {\n layer.reqs = 1;\n q.push(layer);\n }\n};\n\nLTCp.dequeue = function (pxRatio) {\n var self = this;\n var q = self.layersQueue;\n var deqd = [];\n var eleDeqs = 0;\n\n while (eleDeqs < maxDeqSize$1) {\n if (q.size() === 0) {\n break;\n }\n\n var layer = q.peek(); // if a layer has been or will be replaced, then don't waste time with it\n\n if (layer.replacement) {\n // log('layer %s in queue skipped b/c it already has a replacement', layer.id);\n q.pop();\n continue;\n } // if this is a replacement layer that has been superceded, then forget it\n\n\n if (layer.replaces && layer !== layer.replaces.replacement) {\n // log('layer is no longer the most uptodate replacement; dequeued', layer.id)\n q.pop();\n continue;\n }\n\n if (layer.invalid) {\n // log('replacement layer %s is invalid; dequeued', layer.id);\n q.pop();\n continue;\n }\n\n var ele = layer.elesQueue.shift();\n\n if (ele) {\n // log('dequeue layer %s', layer.id);\n self.drawEleInLayer(layer, ele, layer.level, pxRatio);\n eleDeqs++;\n }\n\n if (deqd.length === 0) {\n // we need only one entry in deqd to queue redrawing etc\n deqd.push(true);\n } // if the layer has all its eles done, then remove from the queue\n\n\n if (layer.elesQueue.length === 0) {\n q.pop();\n layer.reqs = 0; // log('dequeue of layer %s complete', layer.id);\n // when a replacement layer is dequeued, it replaces the old layer in the level\n\n if (layer.replaces) {\n self.applyLayerReplacement(layer);\n }\n\n self.requestRedraw();\n }\n }\n\n return deqd;\n};\n\nLTCp.applyLayerReplacement = function (layer) {\n var self = this;\n var layersInLevel = self.layersByLevel[layer.level];\n var replaced = layer.replaces;\n var index = layersInLevel.indexOf(replaced); // if the replaced layer is not in the active list for the level, then replacing\n // refs would be a mistake (i.e. overwriting the true active layer)\n\n if (index < 0 || replaced.invalid) {\n // log('replacement layer would have no effect', layer.id);\n return;\n }\n\n layersInLevel[index] = layer; // replace level ref\n // replace refs in eles\n\n for (var i = 0; i < layer.eles.length; i++) {\n var _p = layer.eles[i]._private;\n var cache = _p.imgLayerCaches = _p.imgLayerCaches || {};\n\n if (cache) {\n cache[layer.level] = layer;\n }\n } // log('apply replacement layer %s over %s', layer.id, replaced.id);\n\n\n self.requestRedraw();\n};\n\nLTCp.requestRedraw = util(function () {\n var r = this.renderer;\n r.redrawHint('eles', true);\n r.redrawHint('drag', true);\n r.redraw();\n}, 100);\nLTCp.setupDequeueing = defs.setupDequeueing({\n deqRedrawThreshold: deqRedrawThreshold$1,\n deqCost: deqCost$1,\n deqAvgCost: deqAvgCost$1,\n deqNoDrawCost: deqNoDrawCost$1,\n deqFastCost: deqFastCost$1,\n deq: function deq(self, pxRatio) {\n return self.dequeue(pxRatio);\n },\n onDeqd: noop,\n shouldRedraw: trueify,\n priority: function priority(self) {\n return self.renderer.beforeRenderPriorities.lyrTxrDeq;\n }\n});\n\nvar CRp = {};\nvar impl;\n\nfunction polygon(context, points) {\n for (var i = 0; i < points.length; i++) {\n var pt = points[i];\n context.lineTo(pt.x, pt.y);\n }\n}\n\nfunction triangleBackcurve(context, points, controlPoint) {\n var firstPt;\n\n for (var i = 0; i < points.length; i++) {\n var pt = points[i];\n\n if (i === 0) {\n firstPt = pt;\n }\n\n context.lineTo(pt.x, pt.y);\n }\n\n context.quadraticCurveTo(controlPoint.x, controlPoint.y, firstPt.x, firstPt.y);\n}\n\nfunction triangleTee(context, trianglePoints, teePoints) {\n if (context.beginPath) {\n context.beginPath();\n }\n\n var triPts = trianglePoints;\n\n for (var i = 0; i < triPts.length; i++) {\n var pt = triPts[i];\n context.lineTo(pt.x, pt.y);\n }\n\n var teePts = teePoints;\n var firstTeePt = teePoints[0];\n context.moveTo(firstTeePt.x, firstTeePt.y);\n\n for (var i = 1; i < teePts.length; i++) {\n var pt = teePts[i];\n context.lineTo(pt.x, pt.y);\n }\n\n if (context.closePath) {\n context.closePath();\n }\n}\n\nfunction circleTriangle(context, trianglePoints, rx, ry, r) {\n if (context.beginPath) {\n context.beginPath();\n }\n\n context.arc(rx, ry, r, 0, Math.PI * 2, false);\n var triPts = trianglePoints;\n var firstTrPt = triPts[0];\n context.moveTo(firstTrPt.x, firstTrPt.y);\n\n for (var i = 0; i < triPts.length; i++) {\n var pt = triPts[i];\n context.lineTo(pt.x, pt.y);\n }\n\n if (context.closePath) {\n context.closePath();\n }\n}\n\nfunction circle(context, rx, ry, r) {\n context.arc(rx, ry, r, 0, Math.PI * 2, false);\n}\n\nCRp.arrowShapeImpl = function (name) {\n return (impl || (impl = {\n 'polygon': polygon,\n 'triangle-backcurve': triangleBackcurve,\n 'triangle-tee': triangleTee,\n 'circle-triangle': circleTriangle,\n 'triangle-cross': triangleTee,\n 'circle': circle\n }))[name];\n};\n\nvar CRp$1 = {};\n\nCRp$1.drawElement = function (context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity) {\n var r = this;\n\n if (ele.isNode()) {\n r.drawNode(context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity);\n } else {\n r.drawEdge(context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity);\n }\n};\n\nCRp$1.drawElementOverlay = function (context, ele) {\n var r = this;\n\n if (ele.isNode()) {\n r.drawNodeOverlay(context, ele);\n } else {\n r.drawEdgeOverlay(context, ele);\n }\n};\n\nCRp$1.drawCachedElementPortion = function (context, ele, eleTxrCache, pxRatio, lvl, reason, getRotation, getOpacity) {\n var r = this;\n var bb = eleTxrCache.getBoundingBox(ele);\n\n if (bb.w === 0 || bb.h === 0) {\n return;\n } // ignore zero size case\n\n\n var eleCache = eleTxrCache.getElement(ele, bb, pxRatio, lvl, reason);\n\n if (eleCache != null) {\n var opacity = getOpacity(r, ele);\n\n if (opacity === 0) {\n return;\n }\n\n var theta = getRotation(r, ele);\n var x1 = bb.x1,\n y1 = bb.y1,\n w = bb.w,\n h = bb.h;\n var x, y, sx, sy, smooth;\n\n if (theta !== 0) {\n var rotPt = eleTxrCache.getRotationPoint(ele);\n sx = rotPt.x;\n sy = rotPt.y;\n context.translate(sx, sy);\n context.rotate(theta);\n smooth = r.getImgSmoothing(context);\n\n if (!smooth) {\n r.setImgSmoothing(context, true);\n }\n\n var off = eleTxrCache.getRotationOffset(ele);\n x = off.x;\n y = off.y;\n } else {\n x = x1;\n y = y1;\n }\n\n var oldGlobalAlpha;\n\n if (opacity !== 1) {\n oldGlobalAlpha = context.globalAlpha;\n context.globalAlpha = oldGlobalAlpha * opacity;\n }\n\n context.drawImage(eleCache.texture.canvas, eleCache.x, 0, eleCache.width, eleCache.height, x, y, w, h);\n\n if (opacity !== 1) {\n context.globalAlpha = oldGlobalAlpha;\n }\n\n if (theta !== 0) {\n context.rotate(-theta);\n context.translate(-sx, -sy);\n\n if (!smooth) {\n r.setImgSmoothing(context, false);\n }\n }\n } else {\n eleTxrCache.drawElement(context, ele); // direct draw fallback\n }\n};\n\nvar getZeroRotation = function getZeroRotation() {\n return 0;\n};\n\nvar getLabelRotation = function getLabelRotation(r, ele) {\n return r.getTextAngle(ele, null);\n};\n\nvar getSourceLabelRotation = function getSourceLabelRotation(r, ele) {\n return r.getTextAngle(ele, 'source');\n};\n\nvar getTargetLabelRotation = function getTargetLabelRotation(r, ele) {\n return r.getTextAngle(ele, 'target');\n};\n\nvar getOpacity = function getOpacity(r, ele) {\n return ele.effectiveOpacity();\n};\n\nvar getTextOpacity = function getTextOpacity(e, ele) {\n return ele.pstyle('text-opacity').pfValue * ele.effectiveOpacity();\n};\n\nCRp$1.drawCachedElement = function (context, ele, pxRatio, extent, lvl, requestHighQuality) {\n var r = this;\n var _r$data = r.data,\n eleTxrCache = _r$data.eleTxrCache,\n lblTxrCache = _r$data.lblTxrCache,\n slbTxrCache = _r$data.slbTxrCache,\n tlbTxrCache = _r$data.tlbTxrCache;\n var bb = ele.boundingBox();\n var reason = requestHighQuality === true ? eleTxrCache.reasons.highQuality : null;\n\n if (bb.w === 0 || bb.h === 0 || !ele.visible()) {\n return;\n }\n\n if (!extent || boundingBoxesIntersect(bb, extent)) {\n var isEdge = ele.isEdge();\n\n var badLine = ele.element()._private.rscratch.badLine;\n\n r.drawCachedElementPortion(context, ele, eleTxrCache, pxRatio, lvl, reason, getZeroRotation, getOpacity);\n\n if (!isEdge || !badLine) {\n r.drawCachedElementPortion(context, ele, lblTxrCache, pxRatio, lvl, reason, getLabelRotation, getTextOpacity);\n }\n\n if (isEdge && !badLine) {\n r.drawCachedElementPortion(context, ele, slbTxrCache, pxRatio, lvl, reason, getSourceLabelRotation, getTextOpacity);\n r.drawCachedElementPortion(context, ele, tlbTxrCache, pxRatio, lvl, reason, getTargetLabelRotation, getTextOpacity);\n }\n\n r.drawElementOverlay(context, ele);\n }\n};\n\nCRp$1.drawElements = function (context, eles) {\n var r = this;\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n r.drawElement(context, ele);\n }\n};\n\nCRp$1.drawCachedElements = function (context, eles, pxRatio, extent) {\n var r = this;\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n r.drawCachedElement(context, ele, pxRatio, extent);\n }\n};\n\nCRp$1.drawCachedNodes = function (context, eles, pxRatio, extent) {\n var r = this;\n\n for (var i = 0; i < eles.length; i++) {\n var ele = eles[i];\n\n if (!ele.isNode()) {\n continue;\n }\n\n r.drawCachedElement(context, ele, pxRatio, extent);\n }\n};\n\nCRp$1.drawLayeredElements = function (context, eles, pxRatio, extent) {\n var r = this;\n var layers = r.data.lyrTxrCache.getLayers(eles, pxRatio);\n\n if (layers) {\n for (var i = 0; i < layers.length; i++) {\n var layer = layers[i];\n var bb = layer.bb;\n\n if (bb.w === 0 || bb.h === 0) {\n continue;\n }\n\n context.drawImage(layer.canvas, bb.x1, bb.y1, bb.w, bb.h);\n }\n } else {\n // fall back on plain caching if no layers\n r.drawCachedElements(context, eles, pxRatio, extent);\n }\n};\n\n/* global Path2D */\nvar CRp$2 = {};\n\nCRp$2.drawEdge = function (context, edge, shiftToOriginWithBb) {\n var drawLabel = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true;\n var shouldDrawOverlay = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true;\n var shouldDrawOpacity = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : true;\n var r = this;\n var rs = edge._private.rscratch;\n\n if (shouldDrawOpacity && !edge.visible()) {\n return;\n } // if bezier ctrl pts can not be calculated, then die\n\n\n if (rs.badLine || rs.allpts == null || isNaN(rs.allpts[0])) {\n // isNaN in case edge is impossible and browser bugs (e.g. safari)\n return;\n }\n\n var bb;\n\n if (shiftToOriginWithBb) {\n bb = shiftToOriginWithBb;\n context.translate(-bb.x1, -bb.y1);\n }\n\n var opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1;\n var lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1;\n var lineStyle = edge.pstyle('line-style').value;\n var edgeWidth = edge.pstyle('width').pfValue;\n var lineCap = edge.pstyle('line-cap').value;\n var effectiveLineOpacity = opacity * lineOpacity; // separate arrow opacity would require arrow-opacity property\n\n var effectiveArrowOpacity = opacity * lineOpacity;\n\n var drawLine = function drawLine() {\n var strokeOpacity = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : effectiveLineOpacity;\n context.lineWidth = edgeWidth;\n context.lineCap = lineCap;\n r.eleStrokeStyle(context, edge, strokeOpacity);\n r.drawEdgePath(edge, context, rs.allpts, lineStyle);\n context.lineCap = 'butt'; // reset for other drawing functions\n };\n\n var drawOverlay = function drawOverlay() {\n if (!shouldDrawOverlay) {\n return;\n }\n\n r.drawEdgeOverlay(context, edge);\n };\n\n var drawArrows = function drawArrows() {\n var arrowOpacity = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : effectiveArrowOpacity;\n r.drawArrowheads(context, edge, arrowOpacity);\n };\n\n var drawText = function drawText() {\n r.drawElementText(context, edge, null, drawLabel);\n };\n\n context.lineJoin = 'round';\n var ghost = edge.pstyle('ghost').value === 'yes';\n\n if (ghost) {\n var gx = edge.pstyle('ghost-offset-x').pfValue;\n var gy = edge.pstyle('ghost-offset-y').pfValue;\n var ghostOpacity = edge.pstyle('ghost-opacity').value;\n var effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity;\n context.translate(gx, gy);\n drawLine(effectiveGhostOpacity);\n drawArrows(effectiveGhostOpacity);\n context.translate(-gx, -gy);\n }\n\n drawLine();\n drawArrows();\n drawOverlay();\n drawText();\n\n if (shiftToOriginWithBb) {\n context.translate(bb.x1, bb.y1);\n }\n};\n\nCRp$2.drawEdgeOverlay = function (context, edge) {\n if (!edge.visible()) {\n return;\n }\n\n var overlayOpacity = edge.pstyle('overlay-opacity').value;\n\n if (overlayOpacity === 0) {\n return;\n }\n\n var r = this;\n var usePaths = r.usePaths();\n var rs = edge._private.rscratch;\n var overlayPadding = edge.pstyle('overlay-padding').pfValue;\n var overlayWidth = 2 * overlayPadding;\n var overlayColor = edge.pstyle('overlay-color').value;\n context.lineWidth = overlayWidth;\n\n if (rs.edgeType === 'self' && !usePaths) {\n context.lineCap = 'butt';\n } else {\n context.lineCap = 'round';\n }\n\n r.colorStrokeStyle(context, overlayColor[0], overlayColor[1], overlayColor[2], overlayOpacity);\n r.drawEdgePath(edge, context, rs.allpts, 'solid');\n};\n\nCRp$2.drawEdgePath = function (edge, context, pts, type) {\n var rs = edge._private.rscratch;\n var canvasCxt = context;\n var path;\n var pathCacheHit = false;\n var usePaths = this.usePaths();\n var lineDashPattern = edge.pstyle('line-dash-pattern').pfValue;\n var lineDashOffset = edge.pstyle('line-dash-offset').pfValue;\n\n if (usePaths) {\n var pathCacheKey = pts.join('$');\n var keyMatches = rs.pathCache