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 serivces 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;
					}
				};

				// Deteremine 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 style = new Object;

					// Variables used for all layouts
					var serviceCount = getObjAttr(d, ["serviceCount"], 0);
					var iconTextPadding = getIconTextPadding(d);

					switch($scope.layout) {
					case layouts.UserSupplied.index:
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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";
						attrs["font-size"] = $scope.fontSize + "px";
						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;
						attrs["font-size"] = $scope.fontSize + "px";
						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;
					}

					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 childen of the node that are not
					// in the list of hosts
					if (node.hasOwnProperty("children") &&
								node.children != null) {
						node.childen = 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("chlidren") ||
							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 (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;
									});
							}
							$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;
						}
						// 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) {
						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]) {
							$scope.hostList[host].serviceStatusJSON[service] =
								json.data.servicelist[host][service];
						}
					}
					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 existance 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 
				// curret 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");
					$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 partion 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, artifically 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 supressed
					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 supressed
					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 collapesd/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:
									return (a.parent == b.parent ? 1 : 2) / a.depth;
									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();
			}
		};
	});
