"use strict"; (function () { 'use strict'; angular.module('graphModule') .directive('directedGraph', ['$timeout', '$window', 'ciExplorerModel', '$interval', function ($timeout, $window, ciExplorerModel, $interval) { return { restrict: 'AE', replace: true, scope: { nodes: '=', edges: '=', selections: '=', servicesCount: '=', isFixed: '=', isFullScreen: '=' }, templateUrl: 'components/graph/directed-graph-template.html', link: function (scope, element) { var graphPan = element.find('.directed-graph__pan'), minZoom = 0.4, initialZoom = 1.5, zoomStep = 0.2, verticalLayoutDirection = false; var userAgents = ['Chrome', 'Safari', 'Firefox']; var userAgent = $window.navigator.userAgent; var isIE = true; _.each(userAgents, function (browser) { var reg = new RegExp(browser, 'i'); if (reg.exec(userAgent)) { isIE = false; } }); function generateGraphStyles() { var styleChain = cytoscape.stylesheet() .selector('node[?hover]') .css({ 'content': 'data(label)' }) .selector('node[!hover]') .css({ 'content': 'data(shortLabel)' }) .selector('node') .css({ 'background-fit': "cover", 'font-size': 8, 'shape': 'rectangle', 'background-opacity': 0, 'text-valign': 'bottom', 'text-wrap': 'wrap', 'text-max-width': 80 }) .selector('node.selected') .css({ 'background-fit': "cover", 'font-size': 8, 'shape': 'rectangle', 'background-opacity': 0, 'text-valign': 'bottom' }) .selector('.big-node') .css({ width: 35, height: 35 }) .selector('.small-node[?hover]') .css({ 'content': 'data(label)' }) .selector('.small-node[!hover]') .css({ 'content': 'data(shortLabel)' }) .selector('.small-node') .css({ 'height': 20, 'width': 20, 'background-fit': 'cover', 'font-size': 5, 'background-opacity': 0, 'text-valign': 'bottom' }) .selector('edge') .css({ 'width': 0.3, 'line-color': '#666666', 'target-arrow-shape': 'triangle', 'curve-style': 'straight' }) .selector('.group-node') .css({ 'shape': 'roundrectangle' }) .selector('.collapsed-node') .css({ 'content': 'data(label)', 'color': '#2bb5dc', 'background-color': '#2bb5dc', 'background-opacity': 0.5, 'background-image': 'none', 'border-width': 0.2, 'font-size': 12, 'text-valign': 'center', 'height': 75, 'width': 100 }) .selector('.expanded-node') .css({ 'content': '', 'background-color': '#2bb5dc', 'background-opacity': 0.2, 'background-image': 'none', 'border-width': 0.2 }) .selector('.hidden-node') .css({ 'visibility': 'hidden' }) .selector('.expand-button') .css({ 'height': '30px', 'width': '75px' }); if (isIE) { styleChain .selector('node') .css({ 'background-image': 'styles/img/ci-type-icons/generic_ci.png' }) .selector('node.selected') .css({ 'background-image': 'styles/img/ci-type-icons/generic_ci_selected.png' }) .selector('.collapsed-node') .css({ 'background-image': 'none' }) .selector('.expanded-node') .css({ 'background-image': 'none' }) .selector('.application') .css({ 'background-image': 'styles/img/ci-type-icons/application.png' }) .selector('.application-selected') .css({ 'background-image': 'styles/img/ci-type-icons/application_selected.png' }) .selector('.cluster') .css({ 'background-image': 'styles/img/ci-type-icons/cluster.png' }) .selector('.cluster-selected') .css({ 'background-image': 'styles/img/ci-type-icons/cluster_selected.png' }) .selector('.computersystem') .css({ 'background-image': 'styles/img/ci-type-icons/computer_system.png' }) .selector('.computersystem-selected') .css({ 'background-image': 'styles/img/ci-type-icons/computer_system_selected.png' }) .selector('.database') .css({ 'background-image': 'styles/img/ci-type-icons/database.png' }) .selector('.database-selected') .css({ 'background-image': 'styles/img/ci-type-icons/database_selected.png' }) .selector('.filesystem') .css({ 'background-image': 'styles/img/ci-type-icons/file_system.png' }) .selector('.filesystem-selected') .css({ 'background-image': 'styles/img/ci-type-icons/file_system_selected.png' }) .selector('.group') .css({ 'background-image': 'styles/img/ci-type-icons/group.png' }) .selector('.group-selected') .css({ 'background-image': 'styles/img/ci-type-icons/group_selected.png' }) .selector('.media') .css({ 'background-image': 'styles/img/ci-type-icons/media.png' }) .selector('.media-selected') .css({ 'background-image': 'styles/img/ci-type-icons/media_selected.png' }) .selector('.network') .css({ 'background-image': 'styles/img/ci-type-icons/network.png' }) .selector('.network-selected') .css({ 'background-image': 'styles/img/ci-type-icons/network_selected.png' }) .selector('.people') .css({ 'background-image': 'styles/img/ci-type-icons/people.png' }) .selector('.people-selected') .css({ 'background-image': 'styles/img/ci-type-icons/people_selected.png' }) .selector('.resource') .css({ 'background-image': 'styles/img/ci-type-icons/resource.png' }) .selector('.resource-selected') .css({ 'background-image': 'styles/img/ci-type-icons/resource_selected.png' }) .selector('.service') .css({ 'background-image': 'styles/img/ci-type-icons/service.png' }) .selector('.service-selected') .css({ 'background-image': 'styles/img/ci-type-icons/service_selected.png' }) .selector('.software') .css({ 'background-image': 'styles/img/ci-type-icons/software.png' }) .selector('.software-selected') .css({ 'background-image': 'styles/img/ci-type-icons/software_selected.png' }) .selector('.ups') .css({ 'background-image': 'styles/img/ci-type-icons/ups.png' }) .selector('.ups-selected') .css({ 'background-image': 'styles/img/ci-type-icons/ups_selected.png' }) .selector('.collapsed-node') .css({ 'background-image': 'none' }); } else { styleChain .selector('node') .css({ 'background-image': 'styles/img/ci-type-icons/generic_ci.svg', }) .selector('node.selected') .css({ 'background-image': 'styles/img/ci-type-icons/generic_ci_selected.svg' }) .selector('.collapsed-node') .css({ 'background-image': 'none' }) .selector('.expanded-node') .css({ 'background-image': 'none' }) .selector('.application') .css({ 'background-image': 'styles/img/ci-type-icons/application.svg' }) .selector('.application-selected') .css({ 'background-image': 'styles/img/ci-type-icons/application_selected.svg' }) .selector('.cluster') .css({ 'background-image': 'styles/img/ci-type-icons/cluster.svg' }) .selector('.cluster-selected') .css({ 'background-image': 'styles/img/ci-type-icons/cluster_selected.svg' }) .selector('.computersystem') .css({ 'background-image': 'styles/img/ci-type-icons/computer_system.svg' }) .selector('.computersystem-selected') .css({ 'background-image': 'styles/img/ci-type-icons/computer_system_selected.svg' }) .selector('.database') .css({ 'background-image': 'styles/img/ci-type-icons/database.svg' }) .selector('.database-selected') .css({ 'background-image': 'styles/img/ci-type-icons/database_selected.svg' }) .selector('.filesystem') .css({ 'background-image': 'styles/img/ci-type-icons/file_system.svg' }) .selector('.filesystem-selected') .css({ 'background-image': 'styles/img/ci-type-icons/file_system_selected.svg' }) .selector('.group') .css({ 'background-image': 'styles/img/ci-type-icons/group.svg' }) .selector('.group-selected') .css({ 'background-image': 'styles/img/ci-type-icons/group_selected.svg' }) .selector('.media') .css({ 'background-image': 'styles/img/ci-type-icons/media.svg' }) .selector('.media-selected') .css({ 'background-image': 'styles/img/ci-type-icons/media_selected.svg' }) .selector('.network') .css({ 'background-image': 'styles/img/ci-type-icons/network.svg' }) .selector('.network-selected') .css({ 'background-image': 'styles/img/ci-type-icons/network_selected.svg' }) .selector('.people') .css({ 'background-image': 'styles/img/ci-type-icons/people.svg' }) .selector('.people-selected') .css({ 'background-image': 'styles/img/ci-type-icons/people_selected.svg' }) .selector('.resource') .css({ 'background-image': 'styles/img/ci-type-icons/resource.svg' }) .selector('.resource-selected') .css({ 'background-image': 'styles/img/ci-type-icons/resource_selected.svg' }) .selector('.service') .css({ 'background-image': 'styles/img/ci-type-icons/service.svg' }) .selector('.service-selected') .css({ 'background-image': 'styles/img/ci-type-icons/service_selected.svg' }) .selector('.software') .css({ 'background-image': 'styles/img/ci-type-icons/software.svg' }) .selector('.software-selected') .css({ 'background-image': 'styles/img/ci-type-icons/software_selected.svg' }) .selector('.ups') .css({ 'background-image': 'styles/img/ci-type-icons/ups.svg' }) .selector('.ups-selected') .css({ 'background-image': 'styles/img/ci-type-icons/ups_selected.svg' }); } return styleChain; } _.each(scope.nodes, function (node) { node.data.label = ciExplorerModel.getGraphLabel(node.data.label); node.data.shortLabel = ciExplorerModel.getShortGraphLabel(node.data.label); }); var cy = scope.cy = cytoscape({ container: graphPan[0], style: generateGraphStyles(), elements: { nodes: scope.nodes, edges: scope.edges }, layout: { name: 'dagre', fit: false }, boxSelectionEnabled: false, wheelSensitivity: 0.2, selectionType: 'additive', zoom: 1, ready: function () { $timeout(function () { scope.init(); scope.$emit('impactGraphInit', { state: 0 }); scope.state.isLoading = false; }); } }); var tappedBefore = null; cy.on('tap', function (event) { var tappedNow = event.target; $timeout(function () { tappedBefore = null; }, 300); if (tappedBefore === tappedNow) { tappedNow.trigger('doubleTap'); tappedBefore = null; } else { tappedBefore = tappedNow; } }); cy.on('doubleTap', '.group-node', function (event) { toggleGroupNode(event.target); }); // on double click cy.on('select', 'node', function (event) { var nodes = this; for (var n = 0; n < nodes.length; n++) { var nd = nodes[n]; if (nd.hasClass('group-node')) { continue; } if (nd.data('icon')) { nd.removeClass(nd.data('icon')); nd.addClass(nd.data('icon') + "-selected selected"); } scope.selections.push(nd.data('id')); } cy.boxSelectionEnabled(false); }); cy.on('unselect', 'node', function (event) { var nodes = this; for (var n = 0; n < nodes.length; n++) { var nd = nodes[n]; if (nd.data('icon')) { nd.addClass(nd.data('icon')); nd.removeClass(nd.data('icon') + "-selected selected"); } var idx = scope.selections.indexOf(nd.data('id')); scope.selections.splice(idx, 1); } }); cy.on('mouseover', 'node', function (event) { var node = event.target; node.data('hover', true); }); cy.on('mouseout', 'node', function (event) { var node = event.target; node.data('hover', false); }); scope.init = function () { var roots = cy.nodes().roots(), hasChildren = {}, rootsOutgoingEdges = roots.outgoers().filter('edge'), groupNode; // first, iterate over all roots and find out if they have children _.each(roots, function (root) { var rootId = root.data('id'), successors = root.successors(), leaves = successors.leaves(), groupedNodes = successors.difference(leaves).filter('node'); hasChildren[rootId] = groupedNodes.length > 0; }); _.each(roots, function (root) { root.unselectify(); var rootId = root.data('id'), successors = root.successors(), leaves = successors.leaves(), groupedNodesandEdges = successors.difference(leaves), groupedNodes = groupedNodesandEdges.filter('node'); if (groupNode && hasChildren[rootId]) { cy.add({ group: "edges", data: { source: rootId, target: groupNode[0].data.id } }); } if (groupedNodes.length > 0) { groupedNodes.addClass('small-node'); var nodesToAdd = []; _.each(leaves, function (leaf) { var leafId = leaf.data('id'); var edgesWithGroup = groupedNodes.edgesWith(leaf); if (edgesWithGroup.length > 0) { var edge = { group: "edges", data: { source: "groupNode__" + rootId, target: leafId } }; nodesToAdd.push(edge); var edgesWithRoot = root.edgesWith(leaf).remove(); groupedNodesandEdges = groupedNodesandEdges.difference(edgesWithRoot); } leaf.addClass('big-node'); }); groupNode = [ { group: "nodes", data: { id: "groupNode__" + rootId, groupElements: groupedNodesandEdges.add(rootsOutgoingEdges), parentId: rootId, label: groupedNodes.length + " CIs" }, classes: "group-node collapsed-node" }, { group: "edges", data: { source: rootId, target: "groupNode__" + rootId } } ]; root.addClass('big-node'); groupedNodes.remove(); cy.add(groupNode.concat(nodesToAdd)); } }); layout(); }; scope.state = { isLoading: true, largeData: (scope.nodes.length > 1000) }; scope.switchLayoutDirection = function () { verticalLayoutDirection = !verticalLayoutDirection; layout(); }; var animationDuration = 250, minimumDuration = 10; scope.toggleFullScreen = function () { toggleBrowserFullScreen(); if (scope.isFullScreen) { unmakeCanvasFullScreen(); } else { makeCanvasFullScreen(); } }; function makeCanvasFullScreen() { scope.isFixed = !scope.isFixed; $timeout(function () { scope.isFullScreen = !scope.isFullScreen; resizeCanvas(animationDuration); }, minimumDuration); } function unmakeCanvasFullScreen() { scope.isFullScreen = !scope.isFullScreen; $timeout(function () { scope.isFixed = !scope.isFixed; resizeCanvas(); }, animationDuration); } var stop = $interval(function () { if (!isBrowserFullScreen() && scope.isFullScreen) { unmakeCanvasFullScreen(); } }, 500); scope.$on('$destroy', function () { console.log('directedGraph: unbind events'); $interval.cancel(stop); cy.removeListener('tap'); cy.removeListener('doubletap'); cy.removeListener('select'); cy.removeListener('unselect'); cy.removeListener('mouseover'); cy.removeListener('mouseout'); cy = null; graphPan = null; }); function toggleBrowserFullScreen() { if (!isBrowserFullScreen()) { // current working methods if (document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.documentElement.msRequestFullscreen) { document.documentElement.msRequestFullscreen(); } else if (document.documentElement.mozRequestFullScreen) { document.documentElement.mozRequestFullScreen(); } else if (document.documentElement.webkitRequestFullscreen) { document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); } } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } } } function isBrowserFullScreen() { return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement; } function resizeCanvas(duration) { duration = duration || 0; $timeout(function () { cy.resize(); layout(); }, duration); } scope.resetCanvas = function () { layout(); }; scope.changeZoomLevel = function (zoomType) { var currentZoom = cy.zoom(); if (zoomType === 'in') { currentZoom += zoomStep; } else if (zoomType === 'out') { currentZoom -= zoomStep; } currentZoom = Math.max(minZoom, currentZoom); cy.zoom(currentZoom); }; scope.enableBoxSelection = function () { cy.boxSelectionEnabled(true); }; function collapseGroupNode(groupNode) { var groupElemId = groupNode.data('id'); var parentEdges = groupNode.incomers().filter('edge'), endPointEdges = cy.elements('edge[source = "' + groupElemId + '"]'); parentEdges.show(); endPointEdges.show(); groupNode.descendants().remove(); groupNode.removeClass('expanded-node').addClass('collapsed-node'); } function expandGroupNode(groupNode) { var groupElements = groupNode.data('groupElements'), groupElemId = groupNode.data('id'), parentEdges = groupNode.incomers().filter('edge'), endPointEdges = cy.elements('edge[source = "' + groupElemId + '"]'); if (parentEdges) { parentEdges.hide(); } if (endPointEdges) { endPointEdges.hide(); } expandGroupsWithNode(groupElements, { parent: groupElemId }); groupNode.removeClass('collapsed-node').addClass('expanded-node'); } function expandGroupsWithNode(groupElements, struct) { var parentId = struct.parent; var parentExists = parentId === null || cy.getElementById(parentId).length > 0; if (parentExists) { var jsons = groupElements.jsons(); var descs = groupElements.descendants(); var descsEtc = descs.merge(descs.add(groupElements).connectedEdges()); groupElements.remove(); // NB: also removes descendants and their connected edges for (var i = 0; i < groupElements.length; i++) { var json = jsons[i]; if (json.group === 'nodes') { json.data.parent = parentId === null ? undefined : parentId; } } } cy.add(jsons).merge(descsEtc && descsEtc.restore()); } function toggleGroupNode(node) { if (node && node.hasClass('collapsed-node')) { expandGroupNode(node); } else { collapseGroupNode(node); } layout(); } scope.collapseAll = function () { var expandedGroupNodes = cy.$('.group-node.expanded-node'); if (expandedGroupNodes && expandedGroupNodes.length > 0) { _.each(expandedGroupNodes, function (groupNode) { collapseGroupNode(groupNode); }); layout(); } }; scope.expandAll = function () { var collapsedGroupNodes = cy.$('.group-node.collapsed-node'); if (collapsedGroupNodes && collapsedGroupNodes.length > 0) { _.each(collapsedGroupNodes, function (groupNode) { expandGroupNode(groupNode); }); layout(); } }; scope.clearSelection = function () { var selected = cy.$(':selected'); selected.deselect(); }; function layout() { return cy.layout({ name: 'dagre', fit: false, ready: function () { _.each(cy.nodes(), function (node) { if (!verticalLayoutDirection) { var pos = node.position(); node.position({ x: pos.y, y: pos.x }); } }); cy.fit(); if (cy.zoom() > initialZoom) { cy.zoom(initialZoom); } else if (cy.zoom() < minZoom) { cy.zoom(minZoom); } cy.center(); } }).run(); } } }; }]); }());