nagios4/html/js/map-directive.js
2017-05-19 23:37:19 +02:00

3692 lines
101 KiB
JavaScript

angular.module("mapApp")
.directive("nagiosMap", function() {
return {
templateUrl: "map-directive.html",
restrict: "AE",
scope: {
cgiurl: "@cgiurl",
layoutIndex: "@layout",
dimensions: "@dimensions",
ulxValue: "@ulx",
ulyValue: "@uly",
lrxValue: "@lrx",
lryValue: "@lry",
root: "=root",
maxzoom: "=maxzoom",
nolinks: "@nolinks",
notext: "@notext",
nopopups: "@nopopups",
noresize: "@noresize",
noicons: "@noicons",
iconurl: "@iconurl",
updateIntervalValue: "@updateInterval",
lastUpdate: "=lastUpdate",
reload: "@reload",
svgWidth: "=mapWidth",
svgHeight: "=mapHeight",
build: "&build"
},
controller: function($scope, $element, $attrs, $http,
nagiosProcessName, layouts) {
// Contents of the popup
$scope.popupContents = {};
// Layout variables
$scope.diameter = Math.min($scope.svgHeight,
$scope.svgWidth);
$scope.mapZIndex = 20;
$scope.popupZIndex = 40;
$scope.popupPadding = 10;
$scope.fontSize = 10; // px
$scope.minRadius = 5;
// radius of node with zero services
$scope.maxRadiusCount = 20;
// number of services at which to max radius
$scope.maxRadius = 12;
// radius of node with maxRadiusCount+ services
$scope.swellRadius = 4;
// amount by which radius swells when updating
$scope.nodeID = 0;
// Incrementing unique node ID for each node
// Display variables
$scope.layout = parseInt($scope.layoutIndex);
$scope.ulx = parseInt($scope.ulxValue);
$scope.uly = parseInt($scope.ulyValue);
$scope.lrx = parseInt($scope.lrxValue);
$scope.lry = parseInt($scope.lryValue);
$scope.showText = $scope.notext == "false";
$scope.showLinks = $scope.nolinks == "false";
$scope.showPopups = $scope.nopopups == "false";
$scope.allowResize = $scope.noresize == "false";
$scope.showIcons = $scope.noicons == "false";
// Resize handle variables
$scope.handleHeight = 8;
$scope.handleWidth = 8;
$scope.handlePadding = 2;
// Host node tree - initialize the root node
$scope.hostTree = {
hostInfo: {
name: nagiosProcessName,
objectJSON: {
name: nagiosProcessName,
icon_image: "",
icon_image_alt: "",
x_2d: 0,
y_2d: 0
},
serviceCount: 0
},
saveArc: {
x: 0,
dx: 0,
y: 0,
dy: 0
},
saveLabel: {
x: 0,
dx: 0,
y: 0,
dy: 0
}
};
$scope.hostList = new Object;
// Icon information
$scope.iconList = new Object;
$scope.iconsLoading = 0;
// Update frequency
$scope.updateStatusInterval =
parseInt($scope.updateIntervalValue) * 1000;
// Map update variables
$scope.updateDuration = 0;
// Date format for popup dates
$scope.popupDateFormat = d3.time.format("%m-%d-%Y %H:%M:%S");
// Root node name
$scope.rootNodeName = nagiosProcessName;
$scope.rootNode = null;
// Application state variables
$scope.fetchingHostlist = false;
$scope.displayPopup = false;
var previousLayout = -1;
var statusTimeout = null;
var displayMapDone = false;
// Placeholder for saving icon url
var previousIconUrl;
// User-supplied layout information
var userSuppliedLayout = {
dimensions: {
upperLeft: {},
lowerRight: {}
},
xScale: d3.scale.linear(),
yScale: d3.scale.linear()
}
// Force layout information
var forceLayout = new Object;
// Watch for changes on the reload value
$scope.$watch("reload", function(newValue) {
// Cancel the timeout if necessary
if (statusTimeout != null) {
clearTimeout(statusTimeout);
}
// Clean up after previous maps
var selectionExit;
switch (previousLayout) {
case layouts.UserSupplied.index:
selectionExit = d3.select("g#container")
.selectAll("g.node")
.data([])
.exit();
selectionExit.selectAll("circle").remove();
selectionExit.selectAll("text").remove();
selectionExit.remove();
break;
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
selectionExit = d3.select("g#container")
.selectAll(".node")
.data([])
.exit();
selectionExit.selectAll("circle").remove();
selectionExit.selectAll("text").remove();
selectionExit.remove();
d3.select("g#container")
.select("g#links")
.selectAll(".link")
.data([])
.exit()
.remove();
d3.select("g#links").remove();
break;
case layouts.CircularMarkup.index:
d3.select("g#container")
.select("g#paths")
.selectAll("path")
.data([])
.remove();
selectionExit = d3.select("g#container")
.selectAll("g.label")
.data([])
.exit();
selectionExit.selectAll("rect").remove();
selectionExit.selectAll("text").remove();
selectionExit.remove();
d3.select("g#paths").remove();
break;
case layouts.Force.index:
selectionExit = d3.select("g#container")
.selectAll(".link")
.data([])
.exit();
selectionExit.selectAll("line").remove();
selectionExit.remove();
selectionExit = d3.select("g#container")
.selectAll("g.node")
.data([])
.exit();
selectionExit.selectAll("circle").remove();
selectionExit.selectAll("text").remove();
selectionExit.remove();
d3.select("g#links").remove();
break;
}
// Clean up the host list
$scope.hostList = {};
// Clean up the icon image cache if the icon url
// has changed
if (previousIconUrl != $scope.iconurl) {
d3.selectAll("div#image-cache img").remove();
$scope.iconList = new Object;
$scope.iconsLoading = 0;
}
previousIconUrl = $scope.iconurl;
// Reset the zoom and pan
$scope.zoom.translate([0,0]).scale(1);
// Show the map
if ($scope.build()) {
// Determine the new layout
$scope.layout = parseInt($scope.layoutIndex);
// Adjust the container appropriately
d3.select("svg#map g#container")
.attr({
transform: function() {
return getContainerTransform();
}
});
// Layout-specific steps
switch ($scope.layout) {
case layouts.UserSupplied.index:
userSuppliedLayout.dimensionType = $scope.dimensions
break;
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
case layouts.CircularMarkup.index:
case layouts.Force.index:
break;
}
previousLayout = $scope.layout;
// Start the spinner
$scope.spinnerdiv = d3.select("div#spinner");
$scope.fetchingHostlist = true;
$scope.spinner = new Spinner($scope.spinnerOpts)
.spin($scope.spinnerdiv[0][0]);
// Get the host list and move forward
getHostList();
}
});
// Watch for changes in the size of the map
$scope.$watch("svgWidth", function(newValue) {
if (displayMapDone) {
updateOnResize(d3.select("#resize-handle").node());
}
});
$scope.$watch("svgHeight", function(newValue) {
if (displayMapDone) {
updateOnResize(d3.select("#resize-handle").node());
}
});
// Get the services of the children of a specific node
var getServiceList = function() {
var parameters = {
query: "servicelist",
formatoptions: "enumerate bitmask",
details: false
};
var getConfig = {
params: parameters,
withCredentials: true
};
if ($scope.showIcons && $scope.iconsLoading > 0) {
setTimeout(function() {
getServiceList()
}, 10);
return;
}
// Send the JSON query
$http.get($scope.cgiurl + "objectjson.cgi", getConfig)
.error(function(err) {
console.warn(err);
})
.success(function(json) {
// Record the time of the last update
$scope.lastUpdate = json.result.query_time;
for(var host in json.data.servicelist) {
$scope.hostList[host].serviceCount =
json.data.servicelist[host].length;
}
});
};
// Take action on the zoom start
var onZoomStart = function() {
// Hide the popup window
$scope.displayPopup = false;
$scope.$apply("displayPopup");
};
// Take action on the zoom
var onZoom = function() {
// Get the event parameters
var zoomTranslate = $scope.zoom.translate();
var zoomScale = $scope.zoom.scale();
var translate = [];
switch($scope.layout) {
case layouts.UserSupplied.index:
d3.selectAll("g.node")
.attr({
transform: function(d) {
return getNodeTransform(d);
}
});
d3.selectAll("g.node text")
.each(function(d) {
setTextAttrs(d, this);
});
break;
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
d3.selectAll("path.link")
.attr({
d: $scope.diagonal
});
d3.selectAll("g.node")
.attr({
transform: function(d) {
return getNodeTransform(d);
}
});
break;
case layouts.CircularBalloon.index:
// Calculate the real translation taking
// into account the centering
translate = [zoomTranslate[0] +
($scope.svgWidth / 2) * zoomScale,
zoomTranslate[1] +
($scope.svgHeight / 2) * zoomScale];
d3.select("svg#map g#container")
.attr("transform", "translate(" + translate +
")");
d3.selectAll("path.link")
.attr({
d: $scope.diagonal
});
d3.selectAll("g.node")
.attr({
transform: function(d) {
return getNodeTransform(d);
}
});
break;
case layouts.CircularMarkup.index:
// Calculate the real translation taking
// into account the centering
translate = [zoomTranslate[0] +
($scope.svgWidth / 2) * zoomScale,
zoomTranslate[1] +
($scope.svgHeight / 2) * zoomScale];
// Update the group with the new calculated values
d3.select("svg#map g#container")
.attr("transform",
"translate(" + translate + ")");
d3.selectAll("path")
.attr("transform", "scale(" + zoomScale + ")");
d3.selectAll("g.label")
.attr({
transform: function(d) {
return getPartitionLabelGroupTransform(d);
}
});
break;
case layouts.Force.index:
d3.selectAll("line.link")
.attr({
x1: function(d) {
return $scope.xZoomScale(d.source.x);
},
y1: function(d) {
return $scope.yZoomScale(d.source.y);
},
x2: function(d) {
return $scope.xZoomScale(d.target.x);
},
y2: function(d) {
return $scope.yZoomScale(d.target.y);
}
});
d3.selectAll("g.node")
.attr({
transform: function(d) {
return "translate(" +
$scope.xZoomScale(d.x) + ", " +
$scope.yZoomScale(d.y) + ")";
}
});
break;
}
};
// Get the tree size
var getTreeSize = function() {
switch($scope.layout) {
case layouts.DepthLayers.index:
return [$scope.svgWidth, $scope.svgHeight -
layouts.DepthLayers.topPadding -
layouts.DepthLayers.bottomPadding];
break;
case layouts.DepthLayersVertical.index:
return [$scope.svgHeight, $scope.svgWidth -
layouts.DepthLayersVertical.leftPadding -
layouts.DepthLayersVertical.rightPadding];
break;
case layouts.CollapsedTree.index:
return [$scope.svgWidth, $scope.svgHeight -
layouts.CollapsedTree.topPadding -
layouts.CollapsedTree.bottomPadding];
break;
case layouts.CollapsedTreeVertical.index:
return [$scope.svgHeight, $scope.svgWidth -
layouts.CollapsedTreeVertical.leftPadding -
layouts.CollapsedTreeVertical.rightPadding];
break;
case layouts.BalancedTree.index:
return [$scope.svgWidth, $scope.svgHeight -
layouts.BalancedTree.topPadding -
layouts.BalancedTree.bottomPadding];
break;
case layouts.BalancedTreeVertical.index:
return [$scope.svgHeight, $scope.svgWidth -
layouts.BalancedTreeVertical.leftPadding -
layouts.BalancedTreeVertical.rightPadding];
break;
case layouts.CircularBalloon.index:
return [360, $scope.diameter / 2 -
layouts.CircularBalloon.outsidePadding];
break;
}
};
// Get the node transform
var getNodeTransform = function(d) {
switch($scope.layout) {
case layouts.UserSupplied.index:
var x1 = d.hostInfo.objectJSON.x_2d;
var x2 = userSuppliedLayout.xScale(x1);
var x3 = $scope.xZoomScale(x2);
var y1 = d.hostInfo.objectJSON.y_2d;
var y2 = userSuppliedLayout.yScale(y1);
var y3 = $scope.yZoomScale(y2);
return "translate(" + x3 + "," + y3 + ")";
break;
case layouts.DepthLayers.index:
case layouts.CollapsedTree.index:
case layouts.BalancedTree.index:
return "translate(" + $scope.xZoomScale(d.x) + "," +
$scope.yZoomScale(d.y) + ")";
break;
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTreeVertical.index:
return "translate(" + $scope.xZoomScale(d.y) + "," +
$scope.yZoomScale(d.x) + ")";
break;
case layouts.CircularBalloon.index:
if(d.y == 0) return "";
var rotateAngle = d.x +
layouts.CircularBalloon.rotation;
var translate = d.y * $scope.zoom.scale();
return "rotate(" + rotateAngle + ") translate(" +
translate + ")";
break;
}
};
// Determine the amount of text padding due to an icon
var getIconTextPadding = function(d) {
var iconHeight = 0, iconWidth = 0;
if (d.hostInfo.hasOwnProperty("iconInfo")) {
iconHeight = d.hostInfo.iconInfo.height;
iconWidth = d.hostInfo.iconInfo.width;
}
else {
return 0;
}
switch($scope.layout) {
case layouts.UserSupplied.index:
switch(layouts.UserSupplied.textAlignment) {
case "above":
case "below":
return iconHeight / 2;
break;
case "left":
case "right":
return iconWidth / 2;
break;
}
break;
case layouts.DepthLayers.index:
case layouts.CollapsedTree.index:
case layouts.BalancedTree.index:
return iconHeight / 2;
break;
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTreeVertical.index:
return iconWidth / 2;
break;
case layouts.CircularBalloon.index:
var rotateAngle = d.x +
layouts.CircularBalloon.rotation;
var angle; // angle used to calculate distance
var r; // radius used to calculate distance
if (rotateAngle < 45.0) {
// Text is right of icon
angle = rotateAngle;
r = iconWidth / 2;
}
else if(rotateAngle < 135.0) {
// Text is below icon
angle = Math.abs(90.0 - rotateAngle);
r = iconHeight / 2;
}
else if(rotateAngle < 225.0) {
// Text is left icon
angle = Math.abs(180.0 - rotateAngle);
r = iconWidth / 2;
}
else if(rotateAngle < 315.0) {
// Text is above icon
angle = Math.abs(270.0 - rotateAngle);
r = iconHeight / 2;
}
else {
// Text is right of icon
angle = 360.0 - rotateAngle;
r = iconWidth / 2;
}
var radians = angle * Math.PI / 180.0;
var cos = Math.cos(radians);
return r + (r - r * cos) * cos;
break;
case layouts.CircularMarkup.index:
return 0;
break;
case layouts.Force.index:
return iconWidth / 2;
break;
}
};
// Set the node label attributes
var setTextAttrs = function(d, domNode) {
// Placeholder for attributes
var attrs = new Object;
var state = "ok";
var stateCounts = {};
// Variables used for all layouts
var serviceCount = getObjAttr(d, ["serviceCount"], 0);
var iconTextPadding = getIconTextPadding(d);
var fontSize = $scope.fontSize + "px";
if (d.hostInfo.name == $scope.$parent.search.host)
fontSize = ($scope.fontSize * 2) + "px";
attrs["font-size"] = fontSize;
attrs["font-weight"] = "normal";
attrs["text-decoration"] = "none";
attrs["fill"] = "#000000";
if (d.hostInfo.name != $scope.$parent.search.host && d.hostInfo.hasOwnProperty("serviceStatusJSON")) {
for (var service in d.hostInfo.serviceStatusJSON) {
var state = d.hostInfo.serviceStatusJSON[service];
if(!stateCounts.hasOwnProperty(state))
stateCounts[state] = 0;
stateCounts[state]++;
}
if (stateCounts["critical"])
state = "critical";
else if (stateCounts["warning"])
state = "warning";
else if (stateCounts["unknown"])
state = "unknown";
else if (stateCounts["pending"])
state = "pending";
}
switch($scope.layout) {
case layouts.UserSupplied.index:
var textPadding =
layouts.UserSupplied.textPadding[layouts.UserSupplied.textAlignment];
if (!d.hostInfo.hasOwnProperty("iconInfo")) {
textPadding += $scope.nodeScale(serviceCount);
}
var x = 0;
var y = 0;
switch(layouts.UserSupplied.textAlignment) {
case "above":
y = -(textPadding + iconTextPadding);
attrs["text-anchor"] = "middle";
break;
case "left":
x = -(textPadding + iconTextPadding);
attrs["text-anchor"] = "end";
attrs.dy = ".4em";
break;
case "right":
x = textPadding + iconTextPadding;
attrs["text-anchor"] = "start";
attrs.dy = ".4em";
break;
case "below":
y = textPadding + iconTextPadding;
attrs["text-anchor"] = "middle";
break;
}
attrs.transform = "translate(" + x + "," + y + ")";
break;
case layouts.DepthLayers.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.DepthLayers.dyText + iconTextPadding;
attrs.dy = d.children ? -textPadding : 0;
attrs.transform = d.children ? "" :
"rotate(90) translate(" + textPadding +
", " + (($scope.fontSize / 2) - 1) + ")";
attrs["text-anchor"] = d.children ? "middle" :
"start";
break;
case layouts.DepthLayersVertical.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.DepthLayersVertical.dxText +
iconTextPadding;
attrs.dx = d.children ? -textPadding : textPadding;
attrs.dy = layouts.DepthLayersVertical.dyText;
attrs["text-anchor"] = d.children ? "end" : "start";
break;
case layouts.CollapsedTree.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.CollapsedTree.dyText +
iconTextPadding;
attrs.dy = d.children ? -textPadding : 0;
attrs.transform = d.children ? "" :
"rotate(90) translate(" + textPadding +
", " + (($scope.fontSize / 2) - 1) + ")";
attrs["text-anchor"] = d.children ? "middle" :
"start";
break;
case layouts.CollapsedTreeVertical.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.CollapsedTreeVertical.dxText +
iconTextPadding;
attrs.dx = d.children ? -textPadding : textPadding;
attrs.dy = layouts.CollapsedTreeVertical.dyText;
attrs["text-anchor"] = d.children ? "end" : "start";
break;
case layouts.BalancedTree.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.BalancedTree.dyText +
iconTextPadding;
attrs.dy = d.children ? -textPadding : 0;
attrs.transform = d.children ? "" :
"rotate(90) translate(" + textPadding +
", " + (($scope.fontSize / 2) - 1) + ")";
attrs["text-anchor"] = d.children ? "middle" :
"start";
break;
case layouts.BalancedTreeVertical.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.BalancedTreeVertical.dxText +
iconTextPadding;
attrs.dx = d.children ? -textPadding : textPadding;
attrs.dy = layouts.BalancedTreeVertical.dyText;
attrs["text-anchor"] = d.children ? "end" : "start";
break;
case layouts.CircularBalloon.index:
var textPadding = $scope.nodeScale(serviceCount) +
layouts.CircularBalloon.textPadding +
iconTextPadding;
if(d.y == 0) {
attrs["text-anchor"] = "middle";
attrs.transform = "translate(0,-" +
$scope.fontSize + ")";
}
else if(d.x < 180) {
attrs["text-anchor"] = "start";
attrs.transform = "translate(" + textPadding +
")";
}
else {
attrs["text-anchor"] = "end";
attrs.transform = "rotate(180) translate(-" +
textPadding + ")";
}
attrs.dy = layouts.CircularBalloon.dyText;
break;
case layouts.CircularMarkup.index:
attrs["alignment-baseline"] = "middle";
attrs["text-anchor"] = "middle";
attrs["transform"] = "";
if (d.hostInfo.hasOwnProperty("iconInfo")) {
var rotateAngle = (d.x + d.dx / 2) * 180 / Math.PI +
layouts.CircularBalloon.rotation;
var translate = (d.hostInfo.iconInfo.height +
layouts.CircularMarkup.textPadding) / 2;
attrs["transform"] = "rotate(" + -rotateAngle +
") translate(0, " + translate + ")";
}
else {
if (d.depth > 0 && d.x + d.dx / 2 > Math.PI) {
attrs["transform"] = "rotate(180)";
}
}
break;
case layouts.Force.index:
attrs["alignment-baseline"] = "middle";
attrs["x"] = $scope.nodeScale(serviceCount) +
layouts.Force.textPadding + iconTextPadding;
break;
}
if (d.hostInfo.name == $scope.$parent.search.host) {
attrs["font-weight"] = "bold";
attrs["stroke"] = "red";
attrs["stroke-width"] = "1";
attrs["fill"] = "#0000ff";
} else if (state != "ok") {
attrs["font-weight"] = "bold";
attrs["text-decoration"] = "underline";
switch(state) {
case "critical":attrs["fill"] = "#ff0000"; break;
case "warning": attrs["fill"] = "#b0b214"; break;
case "unknown": attrs["fill"] = "#ff6419"; break;
case "pending": attrs["fill"] = "#cccccc"; break;
}
}
d3.select(domNode).attr(attrs);
};
// Get the quadrant of the mouse pointer within the svg
var getQuadrant = function(mouse, bcr) {
var quadrant = 0;
// mouse is relative to body -
// convert to relative to svg
var mouseX = mouse[0] - bcr.left;
var mouseY = mouse[1] - bcr.top;
if(mouseX < ((bcr.width - bcr.left) / 2)) {
// Left half of svg
if(mouseY < ((bcr.height - bcr.top) / 2)) {
// Top half of svg
quadrant = 2;
}
else {
// Bottom half of svg
quadrant = 3;
}
}
else {
// Right half of svg
if(mouseY < ((bcr.height - bcr.top) / 2)) {
// Top half of svg
quadrant = 1;
}
else {
// Bottom half of svg
quadrant = 4;
}
}
return quadrant;
};
// Display the popup
var displayPopup = function(d) {
// Get the mouse position relative to the body
var body = d3.select("body");
var mouse = d3.mouse(body.node());
// Get the bounding client rect of the div
// containing the map
var bcr = d3.select("div#mapsvg")
.node()
.getBoundingClientRect();
// Hide the popup by setting is z-index to
// less than that of the map div and by
// centering it under the map div
var popup = d3.select("#popup")
.style({
"z-index": $scope.mapZIndex - 1,
left: $scope.svgWidth / 2 + "px",
top: $scope.svgHeight / 2 + "px"
});
// Set it's contents and "display" it (it's still not
// visible because of it's z-index)
setPopupContents(popup, d);
$scope.displayPopup = true;
$scope.$apply("displayPopup");
// Now that it's "displayed", we can get it's size and
// calculate it's proper placement. Do so and set it's
// z-index so it is displayed
var popupBR = popup[0][0].getBoundingClientRect();
var left;
var top;
switch(getQuadrant(mouse, bcr)) {
case 1:
left = mouse[0] - bcr.left - popupBR.width -
$scope.popupPadding;
top = mouse[1] - bcr.top + $scope.popupPadding;
break;
case 2:
left = mouse[0] - bcr.left + $scope.popupPadding;
top = mouse[1] - bcr.top + $scope.popupPadding;
break;
case 3:
left = mouse[0] - bcr.left + $scope.popupPadding;
top = mouse[1] - bcr.top - popupBR.height -
$scope.popupPadding;
break;
case 4:
left = mouse[0] - bcr.left - popupBR.width -
$scope.popupPadding;
top = mouse[1] - bcr.top - popupBR.height -
$scope.popupPadding;
break;
default: // use first quadrant settings
left = mouse[0] - bcr.left - popupBR.width -
$scope.popupPadding;
top = mouse[1] - bcr.top + $scope.popupPadding;
break;
}
popup.style({
"z-index": $scope.popupZIndex,
left: left + "px",
top: top + "px"
});
};
// Prune any deleted hosts from the host tree
var pruneHostTree = function(node) {
if(node.hasOwnProperty("children")) {
node.children = node.children.filter(function(e) {
return e.hostInfo != null;
});
node.children.forEach(function(e) {
pruneHostTree(e);
});
}
};
// Sort the children of a node recursively
var sortChildren = function(node) {
if (node.hasOwnProperty("children")) {
// First sort the children
node.children.sort(function(a, b) {
if (a.hostInfo.objectJSON.name <
b.hostInfo.objectJSON.name) {
return -1;
}
else if (a.hostInfo.objectJSON.name >
b.hostInfo.objectJSON.name) {
return 1;
}
return 0;
});
// Next sort the children of each of these nodes
node.children.forEach(function(e, i, a) {
sortChildren(e);
});
}
};
// Re-parent the tree with a new root
var reparentTree = function(node) {
// The specified node becomes the new node and all
// it's children remain in place relative to it
var newTree = node;
// Visit each parent of the specified node
var currentNode = node;
while (!(currentNode === $scope.hostTree)) {
// First record the parent node of the current node
var parent = currentNode.parent;
// Next remove the current node as a child of
// the parent node
parent.children = parent.children.filter(function(e, i, a) {
if (e === currentNode) {
return false;
}
return true;
});
// Finally add the parent as a child
// to the current node
if (!currentNode.hasOwnProperty("children")) {
currentNode.children = new Array;
}
currentNode.children.push(parent);
// Set the current node the former parent of the
// former current node
currentNode = parent;
}
// Now sort the nodes in the tree
sortChildren(newTree);
// Record the host name for the root node
$scope.rootNodeName = newTree.hostInfo.name;
$scope.rootNode = newTree;
// Assign the new tree
$scope.hostTree = newTree;
$scope.focalPoint = newTree;
};
// Toggle a node
var toggleNode = function(d) {
if (d.children) {
d._children = d.children;
d.children = null;
d.collapsed = true;
}
else {
switch($scope.layout) {
case layouts.CircularMarkup.index:
updateToggledNodes($scope.hostTree,
updateDescendantsOnExpand);
break;
}
d.children = d._children;
d._children = null;
d.collapsed = false;
}
};
// Interpolate the arcs in data space.
var arcTween = function(a) {
var i = d3.interpolate({x: a.saveArc.x,
dx: a.saveArc.dx, y: a.saveArc.y,
dy: a.saveArc.dy}, a);
return function(t) {
var b = i(t);
a.saveArc.x = b.x;
a.saveArc.dx = b.dx;
a.saveArc.y = b.y;
a.saveArc.dy = b.dy;
return $scope.arc(b);
};
}
// Interpolate the node labels in data space.
var labelGroupTween = function(a) {
var i = d3.interpolate({x: a.saveLabel.x,
dx: a.saveLabel.dx, y: a.saveLabel.y,
dy: a.saveLabel.dy}, a);
return function(t) {
var b = i(t);
a.saveLabel.x = b.x;
a.saveLabel.dx = b.dx;
a.saveLabel.y = b.y;
a.saveLabel.dy = b.dy;
return getPartitionLabelGroupTransform(b);
};
}
// Get the partition map label group transform
var getPartitionLabelGroupTransform = function(d) {
var radians = d.x + d.dx / 2;
var rotate = (radians * 180 / Math.PI) - 90;
var exponent = 1 / layouts.CircularMarkup.radialExponent;
var radius = d.y + (d.y / (d.depth * 2));
var translate = Math.pow(radius, exponent) * $scope.zoom.scale();
var transform = "";
if(d.depth == 0) {
transform = "";
}
else {
transform = "rotate(" + rotate + ")" +
" translate(" + translate + ")";
}
return transform;
};
// Find a host in a sorted array of hosts
var findElement = function(list, key, accessor) {
var start = 0;
var end = list.length - 1;
while (start < end) {
var midpoint = parseInt(start +
(end - start + 1) / 2);
if (accessor(list, midpoint) == key) {
return midpoint;
}
else if (key < accessor(list, midpoint)) {
end = midpoint - 1;
}
else {
start = midpoint + 1;
}
}
return null;
};
// Update a node in the host tree
var updateHostTree = function(node, hosts) {
// Sort the hosts array
hosts.sort();
// First remove any children of the node that are not
// in the list of hosts
if (node.hasOwnProperty("children") &&
node.children != null) {
node.children = node.children.filter(function(e) {
return findElement(hosts, e.hostInfo.name,
function(list, index) {
return list[index];
}) != null;
});
// Sort the remaining children
node.children.sort(function(a, b) {
if (a.hostInfo.name == b.hostInfo.name) {
return 0;
}
else if (a.hostInfo.name < b.hostInfo.name) {
return -1;
}
else {
return 1;
}
});
}
// If the node has no children and the host list
// does, create the property and initialize it
if (!node.hasOwnProperty("children") ||
node.children == null) {
node.children = new Array;
}
// Next add any hosts in the list as children
// of the node, if they're not already
hosts.forEach(function(e) {
var childIndex = findElement(node.children, e,
function(list, index) {
return list[index].hostInfo.name;
});
if ($scope.hostList[e]) {
if (childIndex == null) {
// Create the node object
var hostNode = new Object;
// Point the node's host info to the entry in
// the host list
hostNode.hostInfo = $scope.hostList[e];
// And vice versa
if (!$scope.hostList[e].hasOwnProperty("hostNodes")) {
$scope.hostList[e].hostNodes = new Array;
}
if (!$scope.hostList[e].hostNodes.reduce(function(a, b) {
return a && b === hostNode; }, false)) {
$scope.hostList[e].hostNodes.push(hostNode);
}
// Set the parent of this node
hostNode.parent = node;
// Initialize layout information for transitions
hostNode.saveArc = new Object;
hostNode.saveArc.x = 0;
hostNode.saveArc.dx = 0;
hostNode.saveArc.y = 0;
hostNode.saveArc.dy = 0;
hostNode.saveLabel = new Object;
hostNode.saveLabel.x = 0;
hostNode.saveLabel.dx = 0;
hostNode.saveLabel.y = 0;
hostNode.saveLabel.dy = 0;
// Add the node to the parent node's children
node.children.push(hostNode);
// Get the index
childIndex = node.children.length - 1;
}
// Recurse to all children of this host
if ($scope.hostList[e].objectJSON.child_hosts.length > 0) {
var childHosts = $scope.hostList[e].objectJSON.child_hosts;
updateHostTree(node.children[childIndex],
childHosts, hostNode);
}
}
});
};
// Create an ID for an img based on a file name
var imgID = function(image) {
return "cache-" + image.replace(/\./, "_");
};
// Update the image icon cache
var updateImageIconCache = function() {
var cache = d3.select("div#image-cache")
for (var host in $scope.hostList) {
var image =
$scope.hostList[host].objectJSON.icon_image;
if (image != "") {
if (!$scope.iconList.hasOwnProperty(imgID(image))) {
$scope.iconList[imgID(image)] = new Object;
$scope.iconsLoading++;
cache.append("img")
.attr({
id: function() {
return imgID(image);
},
src: $scope.iconurl + image
})
.on("load", function() {
$scope.iconsLoading--;
var img = d3.select(d3.event.target);
var image = img.attr("id");
$scope.iconList[image].width =
img.node().naturalWidth;
$scope.iconList[image].height =
img.node().naturalHeight;
})
.on("error", function() {
$scope.iconsLoading--;
});
}
$scope.hostList[host].iconInfo =
$scope.iconList[imgID(image)];
}
}
};
// Build the host list and tree from the hosts returned
// from the JSON CGIs
var processHostList = function(json) {
// First prune any host from the host list that
// is no longer in the hosts returned from the CGIs
for (var host in $scope.hostList) {
if(host != nagiosProcessName &&
!json.data.hostlist.hasOwnProperty(host)) {
// Mark the entry as null (deletion is slow)
$scope.hostList[host] = null;
}
}
// Next prune any deleted hosts from the host tree
pruneHostTree($scope.hostTree);
// Now update the host list with the data
// returned from the CGIs
for (var host in json.data.hostlist) {
// If we don't know about the host yet, add it to
// the host list
if (!$scope.hostList.hasOwnProperty(host) ||
$scope.hostList[host] == null) {
$scope.hostList[host] = new Object;
$scope.hostList[host].name = host;
$scope.hostList[host].serviceCount = 0;
}
// If a hosts' parent is not in the hostlist (user
// doesn't have permission to view parent) re-parent the
// host directly under the nagios process
for (var parent in json.data.hostlist[host].parent_hosts) {
var prnt = json.data.hostlist[host].parent_hosts[parent];
if (!json.data.hostlist[prnt]) {
var p = json.data.hostlist[host].parent_hosts;
json.data.hostlist[host].parent_hosts.splice(0, 1);
}
}
// Update the information returned
$scope.hostList[host].objectJSON =
json.data.hostlist[host];
}
// Now update the host tree
var rootHosts = new Array;
for (var host in $scope.hostList) {
if ($scope.hostList[host] != null &&
$scope.hostList[host].objectJSON.parent_hosts.length == 0) {
rootHosts.push(host);
}
}
updateHostTree($scope.hostTree, rootHosts);
// Update the icon image cache
if ($scope.showIcons) {
updateImageIconCache();
}
// Finish the host list processing
finishProcessingHostList();
};
var finishProcessingHostList = function() {
if ($scope.showIcons && $scope.iconsLoading > 0) {
setTimeout(function() {
finishProcessingHostList()
}, 10);
return;
}
// If this is the first time the map has
// been displayed...
if($scope.fetchingHostlist) {
// Stop the spinner
$scope.spinner.stop();
$scope.fetchingHostlist = false;
// Display the map
displayMap();
}
// Reparent the tree to specified root host
if ($scope.hostList.hasOwnProperty($scope.root) &&
($scope.rootNode != $scope.hostTree)) {
reparentTree($scope.hostList[$scope.root].hostNodes[0]);
}
// Finally update the map
updateMap($scope.hostTree);
};
// Get list of all hosts
var getHostList = function() {
var parameters = {
query: "hostlist",
formatoptions: "enumerate bitmask",
details: true
};
var getConfig = {
params: parameters,
withCredentials: true
};
// Send the JSON query
$http.get($scope.cgiurl + "objectjson.cgi?", getConfig)
.error(function(err) {
// Stop the spinner
$scope.spinner.stop();
$scope.fetchingHostlist = false;
console.warn(err);
})
.success(function(json) {
// Record the last time Nagios core was started
$scope.lastNagiosStart =
json.result.program_start;
// Record the time of the last update
$scope.lastUpdate = json.result.query_time;
// Process the host list received
processHostList(json);
// Get the services for each host
getServiceList();
// Get the status of each node
getAllStatus(0);
})
};
// Get the node's stroke color
var getNodeStroke = function(hostStatus, collapsed) {
var stroke;
if(collapsed) {
stroke = "blue";
}
else {
switch(hostStatus) {
case "up":
case "down":
case "unreachable":
stroke = getNodeFill(hostStatus, false);
break;
default:
stroke = "#cccccc";
break;
}
}
return stroke;
};
// Get the node's fill color
var getNodeFill = function(hostStatus, dark) {
var fill;
switch(hostStatus) {
case "up":
fill = dark ? "rgb(0, 105, 0)" : "rgb(0, 210, 0)";
break;
case "down":
fill = dark ? "rgb(128, 0, 0)" : "rgb(255, 0, 0)";
break;
case "unreachable":
fill = dark ? "rgb(64, 0, 0)" : "rgb(128, 0, 0)";
break;
default:
switch($scope.layout) {
case layouts.UserSupplied.index:
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
case layouts.Force.index:
fill = "#ffffff";
break;
case layouts.CircularMarkup.index:
fill = "#cccccc";
break;
}
break;
}
return fill;
};
// Get the host status for the current node
var getHostStatus = function(d) {
var hostStatus = "pending";
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("statusJSON")) {
hostStatus = d.hostInfo.statusJSON.status;
}
return hostStatus;
};
// Get the service count for the current node
var getServiceCount = function(d) {
var serviceCount = 0;
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("serviceCount")) {
serviceCount = d.hostInfo.serviceCount;
}
return serviceCount;
};
// Return the glow filter for an icon
var getGlowFilter = function(d) {
if (d.hostInfo.hasOwnProperty("statusJSON")) {
switch (d.hostInfo.statusJSON.status) {
case "up":
return "url(#icon-glow-up)";
break;
case "down":
return "url(#icon-glow-down)";
break;
case "unreachable":
return "url(#icon-glow-unreachable)";
break;
default:
return null;
break;
}
}
else {
return null;
}
};
// Get the text filter
var getTextFilter = function(d) {
if ($scope.showIcons &&
d.hostInfo.hasOwnProperty("iconInfo") &&
d._children) {
return "url(#circular-markup-text-collapsed)";
}
return null;
};
// Get the text stroke color
var getTextStrokeColor = function(d) {
if ($scope.showIcons &&
d.hostInfo.hasOwnProperty("iconInfo") &&
d._children) {
return "white";
}
return null;
};
// Update the node's status
var updateNode = function(domNode) {
var duration = 750;
var selection = d3.select(domNode);
var data = selection.datum();
var hostStatus = getHostStatus(data);
var serviceCount = getServiceCount(data);
switch($scope.layout) {
case layouts.UserSupplied.index:
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
case layouts.Force.index:
selection.select("circle")
.transition()
.duration(duration)
.attr({
r: function() {
return $scope.nodeScale(serviceCount) +
$scope.swellRadius;
}
})
.style({
stroke: function() {
return getNodeStroke(hostStatus,
selection.datum().collapsed);
},
fill: function() {
return getNodeFill(hostStatus, false);
}
})
.transition()
.duration(duration)
.attr({
r: $scope.nodeScale(serviceCount)
});
selection.select("image")
.style({
filter: function(d) {
return getGlowFilter(d);
}
});
selection.select("text")
.each(function() {
setTextAttrs(data, this);
})
.style({
filter: function(d) {
return getTextFilter(d);
},
stroke: function(d) {
return getTextStrokeColor(d);
}
});
break;
case layouts.CircularMarkup.index:
selection
.transition()
.duration(duration)
.style({
fill: function() {
return getNodeFill(hostStatus, true);
},
"fill-opacity": 1,
"stroke-opacity": 1
})
.attrTween("d", arcTween)
.transition()
.duration(duration)
.style({
fill: function() {
return getNodeFill(hostStatus, false);
}
});
break;
}
};
// What to do when getAllStatus succeeds
var onGetAllStatusSuccess = function(json, since) {
// Record the time of the last update
$scope.lastUpdate = json.result.query_time;
// Check whether Nagios has restarted. If so
// re-read the host list
if (json.result.program_start >
$scope.lastNagiosStart) {
getHostList();
}
else {
// Iterate over all hosts and update their status
for (var host in json.data.hostlist) {
if(!$scope.hostList[host].hasOwnProperty("statusJSON") ||
($scope.hostList[host].statusJSON.last_check <
json.data.hostlist[host].last_check)) {
$scope.hostList[host].statusJSON =
json.data.hostlist[host];
if($scope.hostList[host].hasOwnProperty("g")) {
$scope.hostList[host].g.forEach(function(e, i, a) {
updateNode(e);
});
}
}
}
// Send the request for service status
getServiceStatus(since);
// Schedule an update
statusTimeout = setTimeout(function() {
var newSince = (json.result.last_data_update / 1000) -
$scope.updateStatusInterval;
getAllStatus(newSince) },
$scope.updateStatusInterval);
}
};
// Get status of all hosts and their services
var getAllStatus = function(since) {
if ($scope.showIcons && $scope.iconsLoading > 0) {
setTimeout(function() {
getAllStatus()
}, 10);
return;
}
var parameters = {
query: "hostlist",
formatoptions: "enumerate bitmask",
details: true,
hosttimefield: "lastcheck",
starttime: since,
endtime: "-0"
};
var getConfig = {
params: parameters,
withCredentials: true
};
// Send the request for host status
statusTimeout = null;
$http.get($scope.cgiurl + "statusjson.cgi", getConfig)
.error(function(err) {
console.warn(err);
// Schedule an update
statusTimeout = setTimeout(function() { getAllStatus(since) },
$scope.updateStatusInterval);
})
.success(function(json) {
onGetAllStatusSuccess(json, since);
})
};
// What to do when the getting the service status is successful
var onGetServiceStatusSuccess = function(json) {
var serviceCountUpdated = false;
// Record the time of the last update
$scope.lastUpdate = json.result.query_time;
for (var host in json.data.servicelist) {
var serviceStatUpdated = false;
if (!$scope.hostList[host].hasOwnProperty("serviceStatusJSON")) {
$scope.hostList[host].serviceCount =
Object.keys(json.data.servicelist[host]).length;
serviceCountUpdated = true;
$scope.hostList[host].serviceStatusJSON = new Object;
// Since this is the first time we have a
// service count if we have the host status,
// update the node(s).
if ($scope.hostList[host].hasOwnProperty("statusJSON")) {
switch ($scope.layout) {
case layouts.UserSupplied.index:
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
case layouts.Force.index:
if($scope.hostList[host].hasOwnProperty("g")) {
$scope.hostList[host].g.forEach(function(e, i, a) {
updateNode(e);
});
}
break;
}
}
}
else if (Object.keys($scope.hostList[host].serviceStatusJSON).length
< Object.keys(json.data.servicelist[host]).length) {
$scope.hostList[host].serviceCount =
Object.keys(json.data.servicelist[host]).length;
serviceCountUpdated = true;
}
for (service in json.data.servicelist[host]) {
if ($scope.hostList[host].serviceStatusJSON[service] != json.data.servicelist[host][service])
serviceStatUpdated = true;
$scope.hostList[host].serviceStatusJSON[service] =
json.data.servicelist[host][service];
}
if (serviceStatUpdated) {
$scope.hostList[host].g.forEach(function(e, i, a) {
updateNode(e);
});
}
}
if (serviceCountUpdated) {
switch ($scope.layout) {
case layouts.CircularMarkup.index:
updateMap($scope.hostTree);
break;
}
}
};
// Get status of all hosts' services
var getServiceStatus = function(since) {
var parameters = {
query: "servicelist",
formatoptions: "enumerate bitmask",
servicetimefield: "lastcheck",
starttime: since,
endtime: "-0"
};
var getConfig = {
params: parameters,
withCredentials: true
};
// Send the request for service status
$http.get($scope.cgiurl + "statusjson.cgi", getConfig)
.error(function(err) {
console.warn(err);
})
.success(function(json) {
onGetServiceStatusSuccess(json);
});
};
// Get an object attribute in a generic way that checks for
// the existence of all attributes in the hierarchy
var getObjAttr = function(d, attrs, nilval) {
if(d.hasOwnProperty("hostInfo")) {
var obj = d.hostInfo;
for(var i = 0; i < attrs.length; i++) {
if(!obj.hasOwnProperty(attrs[i])) {
return nilval;
}
obj = obj[attrs[i]];
}
return obj;
}
return nilval;
};
// Determine how long an object has been in it's
// current state
var getStateDuration = function(d) {
var now = new Date;
var duration;
var last_state_change = getObjAttr(d,
["statusJSON", "last_state_change"], null);
var program_start = getObjAttr(d,
["statusJSON", "result", "program_start"],
null);
if(last_state_change == null) {
return "unknown";
}
else if(last_state_change == 0) {
duration = now.getTime() - program_start;
}
else {
duration = now.getTime() - last_state_change;
}
return duration;
};
// Get the display value for a state time
var getStateTime = function(time) {
var when = new Date(time);
if(when.getTime() == 0) {
return "unknown";
}
else {
return when;
}
};
// Get the list of parent hosts
var getParentHosts = function(d) {
var parents = getObjAttr(d,
["objectJSON", "parent_hosts"], null);
if(parents == null) {
return "unknown";
}
else if(parents.length == 0) {
return "None (This is a root host)";
}
else {
return parents.join(", ");
}
};
// Get the number of child hosts
var getChildHosts = function(d) {
var children = getObjAttr(d,
["objectJSON", "child_hosts"], null);
if(children == null) {
return "unknown";
}
else {
return children.length;
}
};
// Get a summary of the host's service states
var getServiceSummary = function(d) {
var states = ["ok", "warning", "unknown", "critical",
"pending"];
var stateCounts = {};
if(d.hostInfo.hasOwnProperty("serviceStatusJSON")) {
for (var service in d.hostInfo.serviceStatusJSON) {
var state = d.hostInfo.serviceStatusJSON[service];
if(!stateCounts.hasOwnProperty(state)) {
stateCounts[state] = 0;
}
stateCounts[state]++;
}
}
return stateCounts;
};
// Set the popup contents
var setPopupContents = function(popup, d) {
$scope.popupContents.hostname = getObjAttr(d,
["objectJSON", "name"], "unknown");
if($scope.popupContents.hostname == nagiosProcessName) {
var now = new Date;
$scope.popupContents.alias = nagiosProcessName;
$scope.popupContents.address = window.location.host;
$scope.popupContents.state = "up";
$scope.popupContents.duration = now.getTime() - $scope.lastNagiosStart;
$scope.popupContents.lastcheck = $scope.lastUpdate;
$scope.popupContents.lastchange = $scope.lastNagiosStart;
$scope.popupContents.parents = "";
$scope.popupContents.children = "";
$scope.popupContents.services = null;
} else {
$scope.popupContents.alias = getObjAttr(d,
["objectJSON", "alias"], "unknown");
$scope.popupContents.address = getObjAttr(d,
["objectJSON", "address"], "unknown");
$scope.popupContents.state = getObjAttr(d,
["statusJSON", "status"], "pending");
$scope.popupContents.duration = getStateDuration(d);
$scope.popupContents.lastcheck =
getStateTime(getObjAttr(d,
["statusJSON", "last_check"], 0));
$scope.popupContents.lastchange =
getStateTime(getObjAttr(d,
["statusJSON", "last_state_change"], 0));
$scope.popupContents.parents = getParentHosts(d);
$scope.popupContents.children = getChildHosts(d);
$scope.popupContents.services = getServiceSummary(d);
}
};
// Update the map
var updateMap = function(source, reparent) {
reparent = reparent || false;
switch($scope.layout) {
case layouts.UserSupplied.index:
updateUserSuppliedMap(source);
break;
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
$scope.updateDuration = 500;
updateTreeMap(source);
break;
case layouts.CircularMarkup.index:
$scope.updateDuration = 750;
updatePartitionMap(source, reparent);
break;
case layouts.Force.index:
updateForceMap(source);
break;
}
};
// Update all descendants of a node when collapsing the node
var updateDescendantsOnCollapse = function(root, x, y,
member) {
// Default member to _children
member = member || "_children";
if(root.hasOwnProperty(member) && root[member] !=
null) {
root[member].forEach(function(e, i, a) {
e.x = x;
e.dx = 0;
updateDescendantsOnCollapse(e, x, y,
"children");
});
}
};
// Update all descendants of a node when expanding the node
var updateDescendantsOnExpand = function(root, x, y,
member) {
// Default member to _children
member = member || "_children";
if(root.hasOwnProperty(member) && root[member] !=
null) {
root[member].forEach(function(e, i, a) {
e.saveArc.x = x;
e.saveArc.dx = 0;
e.saveArc.y = y;
e.saveArc.dy = 0;
e.saveLabel.x = x;
e.saveLabel.dx = 0;
e.saveLabel.y = y;
e.saveLabel.dy = 0;
updateDescendantsOnExpand(e, x, y, "children");
});
}
};
// Update the layout information for nodes which are/were
// children of collapsed nodes
var updateToggledNodes = function(root, updater) {
if(root.collapsed) {
if(root.depth == 0) {
updater(root, 0, 0);
}
else {
updater(root, root.x + root.dx / 2,
root.y + root.dy / 2);
}
}
else if(root.hasOwnProperty("children")) {
root.children.forEach(function(e, i, a) {
updateToggledNodes(e, updater);
});
}
};
// The on-click function for partition maps
var onClickPartition = function(d) {
var evt = d3.event;
// If something else (like a pan) is occurring,
// ignore the click
if(d3.event.defaultPrevented) return;
// Hide the popup
$scope.displayPopup = false;
$scope.$apply("displayPopup");
if(evt.shiftKey) {
// Record the new root
$scope.root = d.hostInfo.name;
$scope.$apply('root');
// A shift-click indicates a reparenting
// of the tree
if(d.collapsed) {
// If the node click is collapsed,
// expand it so the tree will have some
// depth after reparenting
toggleNode(d);
}
// Collapse the root node for good animation
toggleNode($scope.hostTree);
updateMap($scope.hostTree, true);
// Waiting until the updating is done...
setTimeout(function() {
// Re-expand the root node so the
// reparenting will occur correctly
toggleNode($scope.hostTree);
// Reparent the tree and redisplay the map
reparentTree(d);
updateMap($scope.hostTree, true);
}, $scope.updateDuration + 50);
}
else {
// A click indicates collapsing or
// expanding the node
toggleNode(d);
updateMap($scope.hostTree);
}
};
// Recalculate the values of the partition
var recalculatePartitionValues = function(node) {
if(node.hasOwnProperty("children") &&
node.children != null) {
node.children.forEach(function(e) {
recalculatePartitionValues(e);
});
node.value = node.children.reduce(function(a, b) {
return a + b.value;
}, 0);
}
else {
node.value = getPartitionNodeValue(node);
}
};
// Recalculate the layout of the partition
var recalculatePartitionLayout = function(node, index) {
index = index || 0;
if(node.depth > 0) {
if(index == 0) {
node.x = node.parent.x;
}
else {
node.x = node.parent.children[index - 1].x +
node.parent.children[index - 1].dx;
}
node.dx = (node.value / node.parent.value) *
node.parent.dx
}
if(node.hasOwnProperty("children") &&
node.children != null) {
node.children.forEach(function(e, i) {
recalculatePartitionLayout(e, i);
});
}
};
// Text filter for labels
var textFilter = function(d) {
return d.collapsed ?
"url(#circular-markup-text-collapsed)" :
"url(#circular-markup-text)";
}
var addPartitionMapTextGroupContents = function(d, node) {
var selection = d3.select(node);
// Append the label
if($scope.showText) {
selection.append("text")
.each(function(d) {
setTextAttrs(d, this);
})
.style({
"fill-opacity": 1e-6,
fill: "white",
filter: function(d) {
return textFilter(d);
}
})
.text(function(d) {
return d.hostInfo.objectJSON.name;
});
}
// Display the node icon if it has one
if($scope.showIcons) {
var image = d.hostInfo.objectJSON.icon_image;
if (image != "" && image != undefined) {
var iconInfo = d.hostInfo.iconInfo;
selection.append("image")
.attr({
"xlink:href": $scope.iconurl + image,
width: iconInfo.width,
height: iconInfo.height,
x: -(iconInfo.width / 2),
y: -((iconInfo.height +
layouts.CircularMarkup.textPadding +
$scope.fontSize) / 2),
transform: function() {
var rotateAngle = (d.x + d.dx / 2) *
180 / Math.PI +
layouts.CircularBalloon.rotation;
return "rotate(" + -rotateAngle + ")";
}
})
.style({
filter: function() {
return getGlowFilter(d);
}
});
}
}
};
// Update the map for partition displays
var updatePartitionMap = function(source, reparent) {
// The svg element that holds it all
var mapsvg = d3.select("svg#map g#container");
// The data for the map
var mapdata = $scope.partition.nodes(source);
if(reparent) {
if($scope.hostTree.collapsed) {
// If this is a reparent operation and
// we're in the collapse phase, shrink
// the root node to nothing
$scope.hostTree.x = 0;
$scope.hostTree.dx = 0;
$scope.hostTree.y = 0;
$scope.hostTree.dy = 0;
}
else {
// Calculate the total value of the 1st level
// children to determine whether we have
// the bug below
var value = $scope.hostTree.children.reduce(function(a, b) {
return a + b.value;
}, 0);
if(value == 2 * $scope.hostTree.value) {
// This appears to be a bug in the
// d3 library where the sum of the
// values of the children of the root
// node is twice what it should be.
// Work around the bug by manually
// adjusting the values.
recalculatePartitionValues($scope.hostTree);
recalculatePartitionLayout($scope.hostTree);
}
}
}
// Update the data for the paths
var path = mapsvg
.select("g#paths")
.selectAll("path")
.data(mapdata, function(d) {
return d.id || (d.id = ++$scope.nodeID);
});
// Update the data for the labels
var labelGroup = mapsvg
.selectAll("g.label")
.data(mapdata, function(d) {
return d.id || (d.id = ++$scope.nodeID);
});
// Traverse the data, artificially setting the layout
//for collapsed children
updateToggledNodes($scope.hostTree,
updateDescendantsOnCollapse);
var pathEnter = path.enter()
.append("path")
.attr({
d: function(d) {
return $scope.arc({x: 0, dx: 0, y: d.y,
dy: d.dy});
}
})
.style({
"fill-opacity": 1e-6,
"stroke-opacity": 1e-6,
stroke: "#fff",
fill: function(d) {
var hostStatus = "pending";
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("statusJSON")) {
hostStatus = d.hostInfo.statusJSON.status;
}
return getNodeFill(hostStatus, false);
}
})
.on("click", function(d) {
onClickPartition(d);
})
.each(function(d) {
// Traverse each node, saving a pointer
// to the node in the hostList to
// facilitate updating later
if(d.hasOwnProperty("hostInfo")) {
if(!d.hostInfo.hasOwnProperty("g")) {
d.hostInfo.g = new Array;
}
d.hostInfo.g.push(this);
}
});
if ($scope.showPopups) {
pathEnter
.on("mouseover", function(d, i) {
if($scope.showPopups &&
d.hasOwnProperty("hostInfo")) {
displayPopup(d);
}
})
.on("mouseout", function(d, i) {
$scope.displayPopup = false;
$scope.$apply("displayPopup");
});
}
labelGroup.enter()
.append("g")
.attr({
class: "label",
transform: function(d) {
return "translate(" +
$scope.arc.centroid({x: 0,
dx: 1e-6, y: d.y, dy: d.dy}) +
")";
}
})
.each(function(d) {
addPartitionMapTextGroupContents(d, this);
});
// Update paths on changes
path.transition()
.duration($scope.updateDuration)
.style({
"fill-opacity": 1,
"stroke-opacity": 1,
fill: function(d) {
var hostStatus = "pending";
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("statusJSON")) {
hostStatus =
d.hostInfo.statusJSON.status;
}
return getNodeFill(hostStatus, false);
}
})
.attrTween("d", arcTween);
// Update label groups on change
labelGroup
.transition()
.duration($scope.updateDuration)
.attrTween("transform", labelGroupTween);
if($scope.showText) {
labelGroup
.selectAll("text")
.style({
"fill-opacity": 1,
filter: function(d) {
return textFilter(d);
}
})
.each(function(d) {
setTextAttrs(d, this);
});
}
if($scope.showIcons) {
labelGroup
.selectAll("image")
.attr({
transform: function(d) {
var rotateAngle = (d.x + d.dx / 2) * 180 / Math.PI +
layouts.CircularBalloon.rotation;
return "rotate(" + -rotateAngle + ")";
}
});
}
// Remove paths when necessary
path.exit()
.transition()
.duration($scope.updateDuration)
.style({
"fill-opacity": 1e-6,
"stroke-opacity": 1e-6
})
.attrTween("d", arcTween)
.remove();
// Remove labels when necessary
if($scope.showText) {
var labelGroupExit = labelGroup.exit();
labelGroupExit.each(function(d) {
var group = d3.select(this);
group.select("text")
.transition()
.duration($scope.updateDuration / 2)
.style({
"fill-opacity": 1e-6
});
group.select("image")
.transition()
.duration($scope.updateDuration)
.style({
"fill-opacity": 1e-6
});
})
.transition()
.duration($scope.updateDuration)
.attrTween("transform", labelGroupTween)
.remove();
}
};
// Traverse the tree, building a list of nodes at each depth
var updateDepthList = function(node) {
if($scope.depthList[node.depth] == null) {
$scope.depthList[node.depth] = new Array;
}
$scope.depthList[node.depth].push(node);
if(node.hasOwnProperty("children") &&
node.children != null) {
node.children.forEach(function(e) {
updateDepthList(e);
});
}
};
// Calculate the layout for the collapsed tree
var calculateCollapsedTreeLayout = function(root) {
// First get the list of nodes at each depth
$scope.depthList = new Array;
updateDepthList(root);
// Then determine the widest layer
var maxWidth = $scope.depthList.reduce(function(a, b) {
return a > b.length ? a : b.length;
}, 0);
// Determine the spacing of nodes based on the max width
var treeSize = getTreeSize();
var spacing = treeSize[0] / maxWidth;
// Re-calculate the layout based on the above
$scope.depthList.forEach(function(layer, depth) {
layer.forEach(function(node, index) {
// Calculate the location index: the
// "index distance" from the center node
var locationIndex =
(index - (layer.length - 1) / 2);
node.x = (treeSize[0] / 2) +
(locationIndex * spacing);
});
});
};
// The on-click function for trees
var onClickTree = function(d) {
var evt = d3.event;
var updateNode = d;
// If something else (like a pan) is occurring,
// ignore the click
if(d3.event.defaultPrevented) return;
// Hide the popup
$scope.displayPopup = false;
$scope.$apply("displayPopup");
if(evt.shiftKey) {
// Record the new root
$scope.root = d.hostInfo.name;
$scope.$apply('root');
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
// Expand the children of the focal point
$scope.focalPoint.children.forEach(function(e) {
if(e.collapsed) {
toggleNode(e);
}
});
// If the focal point is not the root node,
// restore all children of it's parent
if(!($scope.focalPoint === $scope.hostTree)) {
$scope.focalPoint.parent.children =
$scope.focalPoint.parent._children;
delete $scope.focalPoint.parent._children;
$scope.focalPoint.parent.collapsed = false;
}
break;
default:
if(d.collapsed) {
// If the node click is collapsed,
// expand it so the tree will have
// some depth after reparenting
toggleNode(d);
}
break;
}
reparentTree(d);
}
else {
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
if((d === $scope.focalPoint) ||
!(d.hasOwnProperty("children") ||
d.hasOwnProperty("_children"))) {
// Nothing to see here, move on
return;
}
// Restore all the children of the current focal
// point and it's parent (if it is not the root
// of the tree)
$scope.focalPoint.children.forEach(function(e) {
if(e.collapsed) {
toggleNode(e);
}
});
if(!($scope.focalPoint === $scope.hostTree)) {
$scope.focalPoint.parent.children =
$scope.focalPoint.parent._children;
$scope.focalPoint.parent._children = null;
$scope.focalPoint.parent.collapsed = false;
}
// Set the new focal point
$scope.focalPoint = d;
updateNode = (d === $scope.hostTree) ? d :
d.parent;
break;
default:
toggleNode(d);
break;
}
}
updateMap(updateNode);
};
// Add a node group to the tree map
var addTreeMapNodeGroupContents = function(d, node) {
var selection = d3.select(node);
// Display the circle if the node has no icon or
// icons are suppressed
if(!$scope.showIcons ||
d.hostInfo.objectJSON.icon_image == "") {
selection.append("circle")
.attr({
r: 1e-6
});
}
// Display the node icon if it has one
if($scope.showIcons) {
var image = d.hostInfo.objectJSON.icon_image;
if (image != "" && image != undefined) {
var iconInfo = d.hostInfo.iconInfo;
var rotateAngle = null;
if ($scope.layout == layouts.CircularBalloon.index) {
rotateAngle = d.x +
layouts.CircularBalloon.rotation;
}
selection.append("image")
.attr({
"xlink:href": $scope.iconurl + image,
width: iconInfo.width,
height: iconInfo.height,
x: -(iconInfo.width / 2),
y: -(iconInfo.height / 2),
transform: function() {
return "rotate(" + -rotateAngle + ")";
}
})
.style({
filter: function() {
return getGlowFilter(d);
}
});
}
}
// Label the nodes with their host names
if($scope.showText) {
selection.append("text")
.each(function(d) {
setTextAttrs(d, this);
})
.style({
"fill-opacity": 1e-6
})
.text(function(d) {
return d.hostInfo.objectJSON.name;
});
}
// Register event handlers for showing the popups
if($scope.showPopups) {
selection
.on("mouseover", function(d, i) {
if(d.hasOwnProperty("hostInfo")) {
displayPopup(d);
}
})
.on("mouseout", function(d, i) {
$scope.displayPopup = false;
$scope.$apply("displayPopup");
});
}
};
// Update the tree map
var updateTreeMap = function(source) {
var textAttrs;
// The svg element that holds it all
var mapsvg = d3.select("svg#map g#container");
// Build the nodes from the data
var nodes;
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
// If this is a depth layer layout, first update the
// tree based on the current focused node,
updateDepthLayerTree();
// then build the nodes from the data
var root = ($scope.focalPoint === $scope.hostTree) ?
$scope.hostTree : $scope.focalPoint.parent;
nodes = $scope.tree.nodes(root).reverse();
break;
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
// If this is a collapsed tree layout,
// first build the nodes from the data,
nodes = $scope.tree.nodes($scope.hostTree).reverse();
// then re-calculate the positions of the nodes
calculateCollapsedTreeLayout($scope.hostTree);
break;
default:
nodes = $scope.tree.nodes($scope.hostTree).reverse();
break;
}
// ...and the links from the nodes
var links = $scope.tree.links(nodes);
// Create the groups to contain the nodes
var node = mapsvg.selectAll(".node")
.data(nodes, function(d) {
return d.id || (d.id = ++$scope.nodeID);
});
if($scope.showLinks) {
// Create the paths for the links
var link = mapsvg
.select("g#links")
.selectAll(".link")
.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's
// previous position.
link.enter()
.append("path")
.attr({
class: "link",
d: function(d) {
var o = {
x: (source.hasOwnProperty("xOld") ?
source.xOld : source.x) *
$scope.zoom.scale(),
y: (source.hasOwnProperty("yOld") ?
source.yOld : source.y) *
$scope.zoom.scale()
};
return $scope.diagonal({source: o,
target: o});
}
})
.transition()
.duration($scope.updateDuration)
.attr({
d: $scope.diagonal
});
// Transition links to their new position.
link.transition()
.duration($scope.updateDuration)
.attr({
d: $scope.diagonal
});
// Transition exiting nodes to the parent's
// new position.
link.exit().transition()
.duration($scope.updateDuration)
.attr({
d: function(d) {
var o = {
x: source.x * $scope.zoom.scale(),
y: source.y * $scope.zoom.scale()
};
return $scope.diagonal({source: o,
target: o});
}
})
.remove();
}
// Enter any new nodes at the parent's
// previous position.
var nodeEnter = node.enter()
.append("g")
.attr({
class: "node",
transform: function(d) {
return getNodeTransform(source);
}
})
.on("click", function(d) {
onClickTree(d);
})
.each(function(d) {
// Traverse each node, saving a pointer to
// the node in the hostList to facilitate
// updating later
if(d.hasOwnProperty("hostInfo")) {
if(!d.hostInfo.hasOwnProperty("g")) {
d.hostInfo.g = new Array;
}
d.hostInfo.g.push(this);
}
addTreeMapNodeGroupContents(d, this);
});
// Move the nodes to their final destination
var nodeUpdate = node.transition()
.duration($scope.updateDuration)
.attr({
transform: function(d) {
return getNodeTransform(d);
}
});
// Update the node's circle size
nodeUpdate.select("circle")
.attr({
r: function(d) {
var serviceCount = 0;
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("serviceCount")) {
serviceCount = d.hostInfo.serviceCount;
}
return $scope.nodeScale(serviceCount);
}
})
.style({
stroke: function(d) {
var hostStatus = "pending";
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("statusJSON")) {
hostStatus =
d.hostInfo.statusJSON.status;
}
return getNodeStroke(hostStatus,
d.collapsed);
},
fill: function(d) {
var hostStatus = "pending";
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("statusJSON")) {
hostStatus = d.hostInfo.statusJSON.status;
}
return getNodeFill(hostStatus, false);
},
});
// Update the images' filters
nodeUpdate.select("image")
.style({
filter: function(d) {
return getGlowFilter(d);
}
});
// Update the text's opacity
nodeUpdate.select("text")
.each(function(d) {
setTextAttrs(d, this);
})
.style({
"fill-opacity": 1,
filter: function(d) {
return getTextFilter(d);
},
stroke: function(d) {
return getTextStrokeColor(d);
}
});
// Transition exiting nodes to the parent's
// new position.
var nodeExit = node.exit().transition()
.duration($scope.updateDuration)
.attr({
transform: function(d) {
return getNodeTransform(source);
}
})
.remove();
nodeExit.select("circle")
.attr({
r: 1e-6
});
nodeExit.select("text")
.style({
"fill-opacity": 1e-6
});
// Update all nodes associated with the source
if(source.hasOwnProperty("hostInfo") &&
source.hostInfo.hasOwnProperty("g")) {
source.hostInfo.g.forEach(function(e, i, a) {
updateNode(e);
});
}
// Save the old positions for the next transition.
nodes.forEach(function(e) {
e.xOld = e.x;
e.yOld = e.y;
});
};
// Update the tree for the depth layer layout
var updateDepthLayerTree = function() {
// In a depth layer layout, the focal point node is the
// center of the universe; show only it, it's children
// and it's parent (if the focal point node is not the
// root node).
if(!($scope.focalPoint === $scope.hostTree)) {
// For all cases except where the focal point is the
// root node make the focal point the only child of
// it's parent
$scope.focalPoint.parent._children =
$scope.focalPoint.parent.children;
$scope.focalPoint.parent.children = new Array;
$scope.focalPoint.parent.children.push($scope.focalPoint);
$scope.focalPoint.parent.collapsed = true;
}
// Collapse all the children of the focal point
if($scope.focalPoint.hasOwnProperty("children") &&
$scope.focalPoint.children != null) {
$scope.focalPoint.children.forEach(function(e) {
if(!e.collapsed &&
e.hasOwnProperty("children") &&
(e.children.length > 0)) {
toggleNode(e);
}
});
}
};
var addUserSuppliedNodeGroupContents = function(d, node) {
var selection = d3.select(node);
// Display the circle if the node has no icon or
// icons are suppressed
if(!$scope.showIcons ||
d.hostInfo.objectJSON.icon_image == "") {
selection.append("circle")
.attr({
r: 1e-6
});
}
// Display the node icon if it has one
if($scope.showIcons) {
var image = d.hostInfo.objectJSON.icon_image;
if (image != "" && image != undefined) {
var iconInfo = d.hostInfo.iconInfo
selection.append("image")
.attr({
"xlink:href": $scope.iconurl + image,
width: iconInfo.width,
height: iconInfo.height,
x: -(iconInfo.width / 2),
y: -(iconInfo.height / 2),
})
.style({
filter: function() {
return getGlowFilter(d);
}
});
}
}
// Label the nodes with their host names
if($scope.showText) {
selection.append("text")
.each(function(d) {
setTextAttrs(d, this);
})
.text(function(d) {
return d.hostInfo.objectJSON.name;
});
}
// Register event handlers for showing the popups
if($scope.showPopups) {
selection
.on("mouseover", function(d, i) {
if(d.hasOwnProperty("hostInfo")) {
displayPopup(d);
}
})
.on("mouseout", function(d, i) {
$scope.displayPopup = false;
$scope.$apply("displayPopup");
});
}
};
// Update the map that uses configuration-specified
// coordinates
var updateUserSuppliedMap = function(source) {
// Update the scales
calculateUserSuppliedDimensions();
userSuppliedLayout.xScale
.domain([userSuppliedLayout.dimensions.upperLeft.x,
userSuppliedLayout.dimensions.lowerRight.x]);
userSuppliedLayout.yScale
.domain([userSuppliedLayout.dimensions.upperLeft.y,
userSuppliedLayout.dimensions.lowerRight.y]);
// The svg element that holds it all
var mapsvg = d3.select("svg#map g#container");
// Convert the host list into an array
var mapdata = new Array;
for(host in $scope.hostList) {
if(host != null) {
var tmp = new Object;
tmp.hostInfo = $scope.hostList[host];
mapdata.push(tmp);
}
}
// Update the data for the nodes
var node = mapsvg
.selectAll("g.node")
.data(mapdata);
var nodeEnter = node.enter()
.append("g")
.attr({
class: "node",
transform: function(d) {
return getNodeTransform(d);
}
})
.each(function(d) {
// Traverse each node, saving a pointer
// to the node in the hostList to
// facilitate updating later
if(d.hasOwnProperty("hostInfo")) {
if(!d.hostInfo.hasOwnProperty("g")) {
d.hostInfo.g = new Array;
}
d.hostInfo.g.push(this);
}
addUserSuppliedNodeGroupContents(d, this);
});
};
// Tick function for force layout
var onForceTick = function(source) {
if($scope.showLinks) {
forceLayout.link
.attr({
x1: function(d) {
return $scope.xZoomScale(d.source.x);
},
y1: function(d) {
return $scope.yZoomScale(d.source.y);
},
x2: function(d) {
return $scope.xZoomScale(d.target.x);
},
y2: function(d) {
return $scope.yZoomScale(d.target.y);
}
});
}
forceLayout.node
.attr({
transform: function(d) {
return "translate(" +
$scope.xZoomScale(d.x) + ", " +
$scope.yZoomScale(d.y) + ")";
}
});
};
// Flatten the map
var flattenMap = function(root) {
var nodes = [], i = 0;
function recurse(node, depth) {
if(node.children) node.children.forEach(function(e) {
recurse(e, depth + 1);
});
if(!node.id) node.id = ++i;
node.depth = depth;
nodes.push(node);
}
recurse(root, 0);
return nodes;
};
// Handle a click on a node in the force tree
var onClickForce = function(d) {
// Hide the popup
$scope.displayPopup = false;
$scope.$apply("displayPopup");
// Note: reparenting the tree is not implemented
// because the map doesn't appear any different
// after reparenting. However, reparenting would
// affect what is collapsed/expanded when an
// interior node is click, so it eventually may
// make sense.
toggleNode(d);
updateMap(d);
};
// Add the components to the force map node group
var addForceMapNodeGroupContents = function(d, node) {
var selection = d3.select(node);
if(!$scope.showIcons ||
d.hostInfo.objectJSON.icon_image == "") {
selection.append("circle")
.attr({
r: $scope.minRadius
});
}
// Display the node icon if it has one
if ($scope.showIcons) {
var image = d.hostInfo.objectJSON.icon_image;
if (image != "" && image != undefined) {
var iconInfo = d.hostInfo.iconInfo;
var rotateAngle = null;
if ($scope.layout == layouts.CircularBalloon.index) {
rotateAngle = d.x +
layouts.CircularBalloon.rotation;
}
selection.append("image")
.attr({
"xlink:href": $scope.iconurl + image,
width: iconInfo.width,
height: iconInfo.height,
x: -(iconInfo.width / 2),
y: -(iconInfo.height / 2),
transform: function() {
return "rotate(" + -rotateAngle + ")";
}
})
.style({
filter: function() {
return getGlowFilter(d);
}
});
}
}
if ($scope.showText) {
selection.append("text")
.each(function(d) {
setTextAttrs(d, this);
})
.text(function(d) {
return d.hostInfo.objectJSON.name;
})
.style({
filter: function(d) {
return getTextFilter(d);
},
stroke: function(d) {
return getTextStrokeColor(d);
}
});
}
if ($scope.showPopups) {
selection
.on("click", function(d) {
onClickForce(d);
})
.on("mouseover", function(d) {
if($scope.showPopups) {
if(d.hasOwnProperty("hostInfo")) {
displayPopup(d);
}
}
})
.on("mouseout", function(d) {
$scope.displayPopup = false;
$scope.$apply("displayPopup");
});
}
};
// Update the force map
var updateForceMap = function(source) {
// How long must we wait
var duration = 750;
// The svg element that holds it all
var mapsvg = d3.select("svg#map g#container");
// Build the nodes from the data
var nodes = flattenMap($scope.hostTree);
// ...and the links from the nodes
var links = d3.layout.tree().links(nodes);
// Calculate the force parameters
var maxDepth = nodes.reduce(function(a, b) {
return a > b.depth ? a : b.depth;
}, 0);
var diameter = Math.min($scope.svgHeight -
2 * layouts.Force.outsidePadding,
$scope.svgWidth -
2 * layouts.Force.outsidePadding);
var distance = diameter / (maxDepth * 2);
var charge = -30 * (Math.pow(distance, 1.2) / 20);
// Restart the force layout.
$scope.force
.linkDistance(distance)
.charge(charge)
.nodes(nodes)
.links(links)
.start();
if($scope.showLinks) {
// Create the lines for the links
forceLayout.link = mapsvg.select("g#links")
.selectAll(".link")
.data(links, function(d) { return d.target.id; });
// Create new links
forceLayout.link.enter()
.append("line")
.attr({
class: "link",
x1: function(d) {
return $scope.xZoomScale(d.source.x);
},
y1: function(d) {
return $scope.yZoomScale(d.source.y);
},
x2: function(d) {
return $scope.xZoomScale(d.target.x);
},
y2: function(d) {
return $scope.yZoomScale(d.target.y);
}
});
// Remove any old links.
forceLayout.link.exit().remove();
}
// Create the nodes from the data
forceLayout.node = mapsvg.selectAll("g.node")
.data(nodes, function(d) { return d.id; });
// Exit any old nodes.
forceLayout.node.exit().remove();
// Create any new nodes
var nodeEnter = forceLayout.node.enter()
.append("g")
.attr({
class: "node",
transform: function(d) {
return "translate(" +
$scope.xZoomScale(d.x) + ", " +
$scope.yZoomScale(d.y) + ")";
}
})
.each(function(d) {
// Traverse each node, saving a pointer
// to the node in the hostList to
// facilitate updating later
if(d.hasOwnProperty("hostInfo")) {
if(!d.hostInfo.hasOwnProperty("g")) {
d.hostInfo.g = new Array;
}
d.hostInfo.g.push(this);
}
addForceMapNodeGroupContents(d, this);
})
.call($scope.force.drag);
// Update existing nodes
forceLayout.node
.select("circle")
.transition()
.duration(duration)
.attr({
r: function(d) {
return $scope.nodeScale(getServiceCount(d));
}
})
.style({
stroke: function(d) {
return getNodeStroke(getHostStatus(d),
d.collapsed);
},
fill: function(d) {
return getNodeFill(getHostStatus(d), false);
}
});
forceLayout.node
.select("text")
.style({
filter: function(d) {
return getTextFilter(d);
},
stroke: function(d) {
return getTextStrokeColor(d);
}
});
};
// Create the value function
var getPartitionNodeValue = function(d) {
if(d.hasOwnProperty("hostInfo") &&
d.hostInfo.hasOwnProperty("serviceCount")) {
return d.hostInfo.serviceCount == 0 ? 1 :
d.hostInfo.serviceCount;
}
else {
return 1;
}
};
// Calculate the dimensions for the user supplied layout
var calculateUserSuppliedDimensions = function() {
switch ($scope.dimensions) {
case "auto":
// Create a temporary array with pointers
// to the object JSON data
ojdata = new Array;
for(var host in $scope.hostList) {
if(host != null) {
ojdata.push($scope.hostList[host].objectJSON);
}
}
// Determine dimensions based on included objects
userSuppliedLayout.dimensions.upperLeft.x =
ojdata[0].x_2d;
userSuppliedLayout.dimensions.upperLeft.x =
ojdata.reduce(function(a, b) {
return a < b.x_2d ? a : b.x_2d;
});
userSuppliedLayout.dimensions.upperLeft.y =
ojdata[0].y_2d;
userSuppliedLayout.dimensions.upperLeft.y =
ojdata.reduce(function(a, b) {
return a < b.y_2d ? a : b.y_2d;
});
userSuppliedLayout.dimensions.lowerRight.x =
ojdata[0].x_2d;
userSuppliedLayout.dimensions.lowerRight.x =
ojdata.reduce(function(a, b) {
return a > b.x_2d ? a : b.x_2d;
});
userSuppliedLayout.dimensions.lowerRight.y =
ojdata[0].y_2d;
userSuppliedLayout.dimensions.lowerRight.y =
ojdata.reduce(function(a, b) {
return a > b.y_2d ? a : b.y_2d;
});
break;
case "fixed":
userSuppliedLayout.dimensions.upperLeft.x = 0;
userSuppliedLayout.dimensions.upperLeft.y = 0;
userSuppliedLayout.dimensions.lowerRight.x =
$scope.svgWidth;
userSuppliedLayout.dimensions.lowerRight.y =
$scope.svgHeight;
break;
case "user":
userSuppliedLayout.dimensions.upperLeft.x =
$scope.ulx;
userSuppliedLayout.dimensions.upperLeft.y =
$scope.uly;
userSuppliedLayout.dimensions.lowerRight.x =
$scope.lrx;
userSuppliedLayout.dimensions.lowerRight.y =
$scope.lry;
break;
}
};
// What to do when the resize handle is dragged
var onResizeDrag = function() {
// Get the drag event
var event = d3.event;
// Resize the div
$scope.svgWidth = event.x;
$scope.svgHeight = event.y;
// Propagate changes to parent scope (so, for example,
// menu icon is redrown immediately). Note that it
// doesn't seem to matter what we apply, so the
// empty string is applied to decouple this directive
// from it's parent's scope.
$scope.$parent.$apply("");
updateOnResize(this);
};
var updateOnResize = function(resizeHandle) {
d3.select("div#mapsvg")
.style({
height: function() {
return $scope.svgHeight + "px";
},
width: function() {
return $scope.svgWidth + "px";
}
})
$scope.diameter = Math.min($scope.svgHeight,
$scope.svgWidth);
// Update the scales
switch($scope.layout) {
case layouts.UserSupplied.index:
switch($scope.dimensions) {
case "auto":
userSuppliedLayout.xScale.range([0 +
layouts.UserSupplied.padding.left,
$scope.svgWidth -
layouts.UserSupplied.padding.right]);
userSuppliedLayout.yScale.range([0 +
layouts.UserSupplied.padding.top,
$scope.svgHeight -
layouts.UserSupplied.padding.bottom]);
break;
case "fixed":
userSuppliedLayout.dimensions.lowerRight.x =
$scope.svgWidth;
userSuppliedLayout.dimensions.lowerRight.y =
$scope.svgHeight;
// no break;
case "user":
userSuppliedLayout.xScale.range([0,
$scope.svgWidth]);
userSuppliedLayout.yScale.range([0,
$scope.svgHeight]);
break;
}
break;
}
// Resize the svg
d3.select("svg#map")
.style({
height: $scope.svgHeight,
width: $scope.svgWidth
})
// Update the container transform
d3.select("svg#map g#container")
.attr({
transform: function() {
return getContainerTransform();
}
});
// Update the appropriate layout
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
// Update the tree size
$scope.tree.size(getTreeSize())
break;
case layouts.CircularMarkup.index:
// Update the partition size
var radius = $scope.diameter / 2 -
layouts.CircularMarkup.padding;
var exponent = layouts.CircularMarkup.radialExponent;
$scope.partition.size([2 * Math.PI,
Math.pow(radius, exponent)]);
break;
case layouts.Force.index:
$scope.force.size([$scope.svgWidth -
2 * layouts.Force.outsidePadding,
$scope.svgHeight -
2 * layouts.Force.outsidePadding]);
break;
}
// Move the resize handle
if($scope.allowResize) {
d3.select(resizeHandle)
.attr({
transform: function() {
x = $scope.svgWidth -
($scope.handleWidth +
$scope.handlePadding);
y = $scope.svgHeight -
($scope.handleHeight +
$scope.handlePadding);
return "translate(" + x + ", " +
y + ")";
}
});
}
// Update the contents
switch($scope.layout) {
case layouts.UserSupplied.index:
d3.selectAll("g.node circle")
.attr({
transform: function(d) {
return getNodeTransform(d);
}
});
d3.selectAll("g.node text")
.each(function(d) {
setTextAttrs(d, this);
});
break;
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
$scope.updateDuration = 0;
updateTreeMap($scope.hostTree);
break;
case layouts.CircularMarkup.index:
$scope.updateDuration = 0;
updatePartitionMap($scope.hostTree);
break;
case layouts.Force.index:
updateForceMap($scope.hostTree);
break;
}
};
// Set up the resize function
setupResize = function() {
// Create the drag behavior
var drag = d3.behavior.drag()
.origin(function() {
return { x: $scope.svgWidth,
y: $scope.svgHeight };
})
.on("dragstart", function() {
// silence other listeners
d3.event.sourceEvent.stopPropagation();
})
.on("drag", onResizeDrag);
// Create the resize handle
d3.select("svg#map")
.append("g")
.attr({
id: "resize-handle",
transform: function() {
x = $scope.svgWidth - ($scope.handleWidth +
$scope.handlePadding);
y = $scope.svgHeight -
($scope.handleHeight +
$scope.handlePadding);
return "translate(" + x + ", " + y + ")";
}
})
.call(drag)
.append("path")
.attr({
d: function() {
return "M 0 " + $scope.handleHeight +
" L " + $scope.handleWidth + " " +
$scope.handleHeight + " L " +
$scope.handleWidth + " " + 0 +
" L " + 0 + " " +
$scope.handleHeight;
},
stroke: "black",
fill: "black"
});
};
// Get the node container transform
getContainerTransform = function() {
switch($scope.layout) {
case layouts.UserSupplied.index:
case layouts.Force.index:
return null;
break;
case layouts.DepthLayers.index:
return "translate(0, " +
layouts.DepthLayers.topPadding + ")";
break;
case layouts.DepthLayersVertical.index:
return "translate(" +
layouts.DepthLayersVertical.leftPadding +
", 0)";
break;
case layouts.CollapsedTree.index:
return "translate(0, " +
layouts.CollapsedTree.topPadding +
")";
break;
case layouts.CollapsedTreeVertical.index:
return "translate(" +
layouts.CollapsedTreeVertical.leftPadding +
", 0)";
break;
case layouts.BalancedTree.index:
return "translate(0, " +
layouts.BalancedTree.topPadding + ")";
break;
case layouts.BalancedTreeVertical.index:
return "translate(" +
layouts.BalancedTreeVertical.leftPadding +
", 0)";
break;
case layouts.CircularBalloon.index:
case layouts.CircularMarkup.index:
var zoomTranslate = $scope.zoom.translate();
var zoomScale = $scope.zoom.scale();
var translate = [zoomTranslate[0] +
($scope.svgWidth / 2) * zoomScale,
zoomTranslate[1] +
($scope.svgHeight / 2) * zoomScale];
return "transform", "translate(" + translate +
") scale(" + zoomScale + ")";
break;
default:
return null;
break;
}
};
// Display the map
var displayMap = function() {
displayMapDone = false;
// Update the scales
switch($scope.layout) {
case layouts.UserSupplied.index:
switch($scope.dimensions) {
case "auto":
userSuppliedLayout.xScale
.range([0 +
layouts.UserSupplied.padding.left,
$scope.svgWidth -
layouts.UserSupplied.padding.right]);
userSuppliedLayout.yScale
.range([0 +
layouts.UserSupplied.padding.top,
$scope.svgHeight -
layouts.UserSupplied.padding.bottom]);
break;
case "fixed":
case "user":
userSuppliedLayout.xScale
.range([0, $scope.svgWidth]);
userSuppliedLayout.yScale
.range([0, $scope.svgHeight]);
break;
}
break;
}
// Resize the svg
d3.select("svg#map")
.style({
height: $scope.svgHeight,
width: $scope.svgWidth
});
var container = d3.select("g#container");
// Build the appropriate layout
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
case layouts.CircularBalloon.index:
// Append a group for the links
container.append("g")
.attr({
id: "links"
});
// Build the tree
var treeSize = getTreeSize();
$scope.tree = d3.layout.tree()
.size(treeSize)
.separation(function(a, b) {
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTree.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTree.index:
case layouts.BalancedTreeVertical.index:
return a.parent == b.parent ? 1 : 2;
break;
case layouts.CircularBalloon.index:
var d = a.depth > 0 ? a.depth : b.depth, sep;
if (d <= 0)
d = 1;
sep = (a.parent == b.parent ? 1 : 2) / d;
return sep;
break;
}
});
break;
case layouts.CircularMarkup.index:
// Append a group for the links
container.append("g")
.attr({
id: "paths"
});
// Build the partition
var radius = $scope.diameter / 2 -
layouts.CircularMarkup.padding;
var exponent = layouts.CircularMarkup.radialExponent;
$scope.partition = d3.layout.partition()
// .sort(cmpHostName)
.size([2 * Math.PI, Math.pow(radius, exponent)])
.value(getPartitionNodeValue);
break;
case layouts.Force.index:
// Append a group for the links
container.append("g")
.attr({
id: "links"
});
// Build the layout
$scope.force = d3.layout.force()
.size([$scope.svgWidth -
2 * layouts.Force.outsidePadding,
$scope.svgHeight -
2 * layouts.Force.outsidePadding])
.on("tick", onForceTick);
break;
}
// Create the diagonal that will be used to
// connect the nodes
switch($scope.layout) {
case layouts.DepthLayers.index:
case layouts.CollapsedTree.index:
case layouts.BalancedTree.index:
$scope.diagonal = d3.svg.diagonal()
.projection(function(d) {
return [$scope.xZoomScale(d.x),
$scope.yZoomScale(d.y)];
});
break;
case layouts.DepthLayersVertical.index:
case layouts.CollapsedTreeVertical.index:
case layouts.BalancedTreeVertical.index:
$scope.diagonal = d3.svg.diagonal()
.projection(function(d) {
return [$scope.xZoomScale(d.y),
$scope.yZoomScale(d.x)];
});
break;
case layouts.CircularBalloon.index:
$scope.diagonal = d3.svg.diagonal.radial()
.projection(function(d) {
var angle = 0;
if(!isNaN(d.x)) {
angle = d.x +
layouts.CircularBalloon.rotation
+ 90;
}
return [d.y * $scope.zoom.scale(),
((angle / 180) * Math.PI)];
});
break;
}
// Create the arc this will be used to display the nodes
switch($scope.layout) {
case layouts.CircularMarkup.index:
$scope.arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) {
return Math.pow(d.y, (1 / exponent));
})
.outerRadius(function(d) {
return Math.pow(d.y + d.dy, (1 / exponent));
});
break;
}
// Set the focal point to the root
$scope.focalPoint = $scope.hostTree;
// Signal the fact that displayMap() is done
displayMapDone = true;
};
// Activities that take place only on
// directive instantiation
var onDirectiveInstantiation = function() {
// Create the zoom behavior
$scope.xZoomScale = d3.scale.linear();
$scope.yZoomScale = d3.scale.linear();
$scope.zoom = d3.behavior.zoom()
.scaleExtent([1 / $scope.maxzoom, $scope.maxzoom])
.x($scope.xZoomScale)
.y($scope.yZoomScale)
.on("zoomstart", onZoomStart)
.on("zoom", onZoom);
// Set up the div containing the map and
// attach the zoom behavior to the it
d3.select("div#mapsvg")
.style({
"z-index": $scope.mapZIndex,
height: function() {
return $scope.svgHeight + "px";
},
width: function() {
return $scope.svgWidth + "px";
}
})
.call($scope.zoom);
// Set up the resize function
if($scope.allowResize) {
setupResize();
}
// Create a container group
d3.select("svg#map")
.append("g")
.attr({
id: "container",
transform: function() {
return getContainerTransform();
}
});
// Create scale to size nodes based on
// number of services
$scope.nodeScale = d3.scale.linear()
.domain([0, $scope.maxRadiusCount])
.range([$scope.minRadius, $scope.maxRadius])
.clamp(true);
};
onDirectiveInstantiation();
}
};
});