refactor for new topology view
diff --git a/web/ons-demo/assets/logo.svg b/web/ons-demo/assets/logo.svg
new file mode 100644
index 0000000..e451da4
--- /dev/null
+++ b/web/ons-demo/assets/logo.svg
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   id="svg3166"
+   version="1.1"
+   inkscape:version="0.48.0 r9654"
+   width="765"
+   height="990"
+   xml:space="preserve"
+   sodipodi:docname="ON.LAB_logo.eps"><metadata
+     id="metadata3172"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs3170"><clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath3182"><path
+         d="M 0,327.273 0,0 l 1731.3,0 0,327.273 -1731.3,0 z"
+         id="path3184" /></clipPath></defs><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1158"
+     inkscape:window-height="739"
+     id="namedview3168"
+     showgrid="false"
+     inkscape:zoom="0.26464646"
+     inkscape:cx="382.5"
+     inkscape:cy="495"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="g3174" /><g
+     id="g3174"
+     inkscape:groupmode="layer"
+     inkscape:label="ink_ext_XXXXXX"
+     transform="matrix(1.25,0,0,-1.25,0,990)"><g
+       id="g3176"
+       transform="matrix(0.1,0,0,0.1,214.62596,371.81679)"><g
+         id="g3178"><g
+           id="g3180"
+           clip-path="url(#clipPath3182)"><path
+             d="m 164.363,70.2617 c -58.078,0 -101.8982,42.3673 -101.8982,98.5503 0,34.516 21.5743,99.797 103.1802,99.797 70.007,0 101.507,-57.371 101.507,-98.113 0,-23.973 -10.742,-52.461 -26.117,-69.277 C 223.723,82.4297 193.609,70.2617 164.363,70.2617 z m -0.41,257.0113 C 56.3242,327.273 0,247.559 0,168.812 0,79.4414 69.5664,12.0312 161.848,12.0312 c 96.941,0 167.328,66.4727 167.328,158.0508 0,76.203 -57.903,157.191 -165.223,157.191"
+             style="fill:#325cb3;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3186"
+             inkscape:connector-curvature="0" /><path
+             d="m 656.363,323.477 -9.922,0 0,-206.633 -176.695,203.222 -2.965,3.411 -47.797,0 0,-307.6372 58.684,0 0,204.9182 175.043,-201.5002 2.937,-3.418 49.895,0 0,307.6372 -49.18,0"
+             style="fill:#204476;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3188"
+             inkscape:connector-curvature="0" /><path
+             d="m 925.344,59.7148 0,265.7772 -64.188,0 0,-325.492 201.784,0 0,59.7148 -137.596,0"
+             style="fill:#461f35;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3190"
+             inkscape:connector-curvature="0" /><path
+             d="m 1308.12,149.355 -64.91,0 31.25,75.622 33.66,-75.622 z m -8.81,172.036 -2.6,5.882 -43.93,0 -2.57,-6.027 L 1118.88,13.8164 1112.98,0 l 66.39,0 2.59,6.02344 35.94,84.06246 117.03,0 38.15,-84.25778 2.63,-5.82812 66.49,0 -6.19,13.9453 -136.7,307.4457"
+             style="fill:#971a1f;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3192"
+             inkscape:connector-curvature="0" /><path
+             d="m 1621.32,200.641 c -6.74,-3.907 -19.94,-6.028 -36.62,-6.028 l -19.2,0 0,72.035 19.2,0 c 12.13,0 28.75,0 39.33,-4.902 9.69,-4.523 15.73,-15.676 15.73,-29.09 0,-20.129 -12.91,-29.097 -18.44,-32.015 z m -22.25,-141.8246 -33.57,0 0,76.5236 31.29,0 c 24.33,0 36.89,-1.485 46.36,-5.488 21.41,-8.817 23.07,-27.129 23.07,-32.5395 0,-11.5195 -6.66,-27.5156 -25.45,-34.6445 -9.61,-3.8516 -30.49,-3.8516 -41.7,-3.8516 z m 74.77,115.7146 c 20.6,14.879 31.03,35.555 31.03,61.699 0,38.442 -21.17,68.29 -58.02,81.911 -16.27,6.117 -32.93,7.351 -54.54,7.351 l -90.11,0 0,-325.492 94.59,0 c 33.47,0 48.75,0 68.28,6.84375 31.97,10.91015 66.23,39.37505 66.23,86.43745 0,36.6488 -21.9,66.7068 -57.46,81.2498"
+             style="fill:#c41e25;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3194"
+             inkscape:connector-curvature="0" /><path
+             d="m 780.367,59.7148 c -16.476,0 -29.863,-13.3984 -29.863,-29.8632 C 750.504,13.3984 763.891,0 780.367,0 c 16.485,0 29.863,13.3984 29.863,29.8516 0,16.4648 -13.378,29.8632 -29.863,29.8632"
+             style="fill:#461f35;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path3196"
+             inkscape:connector-curvature="0" /></g></g></g></g></svg>
\ No newline at end of file
diff --git a/web/ons-demo/css/layout.default.css b/web/ons-demo/css/layout.default.css
index 5e1f1ce..cfca8c5 100644
--- a/web/ons-demo/css/layout.default.css
+++ b/web/ons-demo/css/layout.default.css
@@ -4,10 +4,24 @@
 
 body {
 	display: -webkit-box;
-	-webkit-box-orient: vertical;
 	-webkit-user-select: none;
 }
 
+#background, #background-image {
+	width: 100%;
+	height: 100%;
+}
+
+#contents {
+	width: 100%;
+	height: 100%;
+	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	position: absolute;
+	top: 0px;
+	left: 0px;
+}
+
 #columns {
 	display: -webkit-box;
 	-webkit-box-flex: 1.0;
diff --git a/web/ons-demo/css/skin.default.css b/web/ons-demo/css/skin.default.css
index 4beb712..a6c636d 100644
--- a/web/ons-demo/css/skin.default.css
+++ b/web/ons-demo/css/skin.default.css
@@ -6,6 +6,11 @@
 	margin: 0px;
 }
 
+#contents {
+	visibility: hidden;
+	background-color: black;
+}
+
 #topology.linking {
 	cursor: crosshair;
 }
diff --git a/web/ons-demo/index.html b/web/ons-demo/index.html
index 55237ac..05c20b4 100644
--- a/web/ons-demo/index.html
+++ b/web/ons-demo/index.html
@@ -5,49 +5,63 @@
 	<link rel="stylesheet" href="css/skin.default.css" type="text/css"/>
 	<script src="js/d3.v3.js" charset="utf-8"></script>
 	<script src="js/async.js"></script>
+	<script src="js/debug.js"></script>
+	<script src="js/constants.js"></script>
+	<script src="js/globals.js"></script>
 	<script src="js/utils.js"></script>
 	<script src="js/model.js"></script>
 	<script src="js/controller.js"></script>
+	<script src="js/controllers.js"></script>
+	<script src="js/topology.js"></script>
+	<script src="js/flows.js"></script>
+	<script src="js/init.js"></script>
 </head>
 
 <body>
+<div id='background'>
+	<img id='background-image' class='pending' src='assets/logo.svg'/>
+</div>
 
-<div id='columns'>
-	<div id='left'>
-		<div class='header'>
-			<img id='logo' src='assets/logo.png'></img>
-		</div>
-		<div id='controllers'>
-			<div class='header'>ONOS Nodes</div>
-			<div id='controllerList'></div>
-		</div>
-	</div>
+<div id='contents'>
 
-	<div id='right'>
-		<div class='header'>
-			<div id='status' class='top'>
-				<div class='status'><span class='dynamic' id='activeFlows'>????</span><span class='static'>Flows</span></div>
-				<div class='status'><span class='dynamic' id='activeSwitches'>???</span><span class='static'>Active Switches</span></div>
+	<div id='columns'>
+		<div id='left'>
+			<div class='header'>
+				<img id='logo' src='assets/logo.png'></img>
 			</div>
-			<div id='traceButton' class='button'>Trace</div>
-			<div id='lastUpdated' class='status top'><span class='static'>Last updated:</span><span id='lastUpdate' class='dynamic'>Mon Mar 18 11:11:12 PDT 2013</span></div>
+			<div id='controllers'>
+				<div class='header'>ONOS Nodes</div>
+				<div id='controllerList'></div>
+			</div>
 		</div>
-		<div id='topology'>
-			<div id='svg-container'></div>
+
+		<div id='right'>
+			<div class='header'>
+				<div id='status' class='top'>
+					<div class='status'><span class='dynamic' id='activeFlows'>????</span><span class='static'>Flows</span></div>
+					<div class='status'><span class='dynamic' id='activeSwitches'>???</span><span class='static'>Active Switches</span></div>
+				</div>
+				<div id='traceButton' class='button'>Trace</div>
+				<div id='lastUpdated' class='status top'><span class='static'>Last updated:</span><span id='lastUpdate' class='dynamic'>Mon Mar 18 11:11:12 PDT 2013</span></div>
+			</div>
+			<div id='topology'>
+				<div id='svg-container'></div>
+			</div>
 		</div>
 	</div>
-</div>
-<div id='selectedFlowsHeader'>
-	<div id='deleteFlow'></div>
-	<div id='showFlowChooser' class='flowId'><div class='white-eye'></div></div>
-	<div class='srcDPID'>src</div>
-	<div class='dstDPID'>dst</div>
-	<div class='iperf'>iperf</div>
-</div>
-<div id='selectedFlows'></div>
-<div id='flowChooser'></div>
+	<div id='selectedFlowsHeader'>
+		<div id='deleteFlow'></div>
+		<div id='showFlowChooser' class='flowId'><div class='white-eye'></div></div>
+		<div class='srcDPID'>src</div>
+		<div class='dstDPID'>dst</div>
+		<div class='iperf'>iperf</div>
+	</div>
+	<div id='selectedFlows'></div>
+	<div id='flowChooser'></div>
 
-<script src="js/app.js"></script>
+	<script src="js/app.js"></script>
+
+</div>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/web/ons-demo/js/app.js b/web/ons-demo/js/app.js
index 5c92490..f7eb5b4 100644
--- a/web/ons-demo/js/app.js
+++ b/web/ons-demo/js/app.js
@@ -1,1206 +1,9 @@
 /*global d3, document∆*/
 
-d3.selection.prototype.moveToFront = function() {
-  return this.each(function(){
-    this.parentNode.appendChild(this);
-  });
-};
-
-var line = d3.svg.line()
-    .x(function(d) {
-    	return d.x;
-    })
-    .y(function(d) {
-    	return d.y;
-    });
-
-var model;
-var svg;
-var updateTopology;
-var pendingLinks = {};
-var selectedFlows = [];
-
-var pendingTimeout = 30000;
-
-var colors = [
-	'color1',
-	'color2',
-	'color3',
-	'color4',
-	'color7',
-	'color8',
-	'color9',
-//	'color11',
-	'color12'
-];
-colors.reverse();
-
-var controllerColorMap = {};
-
-function setPending(selection) {
-	selection.classed('pending', false);
-	setTimeout(function () {
-		selection.classed('pending', true);
-	})
-}
-
-function createTopologyView() {
-
-	window.addEventListener('resize', function () {
-		// this is too slow. instead detect first resize event and hide the paths that have explicit matrix applied
-		// either that or is it possible to position the paths so they get the automatic transform as well?
-//		updateTopology();
-	});
-
-	var svg = d3.select('#svg-container').append('svg:svg');
-
-	svg.append("svg:defs").append("svg:marker")
-	    .attr("id", "arrow")
-	    .attr("viewBox", "0 -5 10 10")
-	    .attr("refX", -1)
-	    .attr("markerWidth", 5)
-	    .attr("markerHeight", 5)
-	    .attr("orient", "auto")
-	  .append("svg:path")
-	    .attr("d", "M0,-3L10,0L0,3");
-
-	return svg.append('svg:svg').attr('id', 'viewBox').attr('viewBox', '0 0 1000 1000').attr('preserveAspectRatio', 'none').
-			attr('id', 'viewbox').append('svg:g').attr('transform', 'translate(500 500)');
-}
-
-function updateSelectedFlowsTopology() {
-	// DRAW THE FLOWS
-	var topologyFlows = [];
-	selectedFlows.forEach(function (flow) {
-		if (flow) {
-			topologyFlows.push(flow);
-		}
-	});
-
-	var flows = d3.select('svg').selectAll('.flow').data(topologyFlows);
-
-	flows.enter().append("svg:path").attr('class', 'flow')
-		.attr('stroke-dasharray', '4, 10')
-		.append('svg:animate')
-		.attr('attributeName', 'stroke-dashoffset')
-		.attr('attributeType', 'xml')
-		.attr('from', '500')
-		.attr('to', '-500')
-		.attr('dur', '20s')
-		.attr('repeatCount', 'indefinite');
-
-	flows.exit().remove();
-
-	flows.attr('d', function (d) {
-			if (!d) {
-				return;
-			}
-			var pts = [];
-			if (!d.dataPath.flowEntries) {
-				// create a temporary vector to indicate the pending flow
-				var s1 = d3.select(document.getElementById(d.srcDpid));
-				var s2 = d3.select(document.getElementById(d.dstDpid));
-
-				var pt1 = document.querySelector('svg').createSVGPoint();
-				pt1.x = s1.attr('x');
-				pt1.y = s1.attr('y');
-				pt1 = pt1.matrixTransform(s1[0][0].getCTM());
-				pts.push(pt1);
-
-				var pt2 = document.querySelector('svg').createSVGPoint();
-				pt2.x = s2.attr('x');
-				pt2.y = s2.attr('y');
-				pt2 = pt2.matrixTransform(s2[0][0].getCTM());
-				pts.push(pt2);
-
-			} else {
-				d.dataPath.flowEntries.forEach(function (flowEntry) {
-					var s = d3.select(document.getElementById(flowEntry.dpid.value));
-					// s[0] is null if the flow entry refers to a non-existent switch
-					if (s[0][0]) {
-						var pt = document.querySelector('svg').createSVGPoint();
-						pt.x = s.attr('x');
-						pt.y = s.attr('y');
-						pt = pt.matrixTransform(s[0][0].getCTM());
-						pts.push(pt);
-					} else {
-						console.log('flow refers to non-existent switch: ' + flowEntry.dpid.value);
-					}
-				});
-			}
-			if (pts.length) {
-				return line(pts);
-			} else {
-				return "M0,0";
-			}
-		})
-		.attr('id', function (d) {
-			if (d) {
-				return makeFlowKey(d);
-			}
-		})
-		.classed('pending', function (d) {
-			return d && (d.createPending || d.deletePending);
-		});
-
-	// "marching ants"
-	flows.select('animate').attr('from', 500);
-
-}
-
-function updateSelectedFlowsTable() {
-	function rowEnter(d) {
-		var row = d3.select(this);
-		row.append('div').classed('deleteFlow', true);
-		row.append('div').classed('flowId', true);
-		row.append('div').classed('srcDPID', true);
-		row.append('div').classed('dstDPID', true);
-		row.append('div').classed('iperf', true);
-
-		row.select('.iperf')
-			.append('div')
-			.attr('class', 'iperf-container')
-			.append('svg:svg')
-			.attr('viewBox', '0 0 1000 32')
-			.attr('preserveAspectRatio', 'none')
-			.append('svg:g')
-			.append('svg:path')
-			.attr('class', 'iperfdata');
-
-		row.on('mouseover', function (d) {
-			if (d) {
-				var path = document.getElementById(makeFlowKey(d));
-				d3.select(path).classed('highlight', true);
-			}
-		});
-		row.on('mouseout', function (d) {
-			if (d) {
-				var path = document.getElementById(makeFlowKey(d));
-				d3.select(path).classed('highlight', false);
-			}
-		});
-	}
-
-	function rowUpdate(d) {
-		var row = d3.select(this);
-		row.attr('id', function (d) {
-			if (d) {
-				return makeSelectedFlowKey(d);
-			}
-		});
-
-		if (!d || !hasIPerf(d)) {
-			row.select('.iperfdata')
-				.attr('d', 'M0,0');
-		}
-
-		row.select('.deleteFlow').on('click', function () {
-			deselectFlow(d);
-		});
-		row.on('dblclick', function () {
-			if (d) {
-				var prompt = 'Delete flow ' + d.flowId + '?';
-				if (confirm(prompt)) {
-					deleteFlow(d);
-					d.deletePending = true;
-					updateSelectedFlows();
-
-					setTimeout(function () {
-						d.deletePending = false;
-						updateSelectedFlows();
-					}, pendingTimeout)
-				};
-			}
-		});
-
-		row.select('.flowId')
-			.text(function (d) {
-				if (d) {
-					if (d.flowId) {
-						return d.flowId;
-					} else {
-						return '0x--';
-					}
-				}
-			})
-			.classed('pending', function (d) {
-				return d && (d.createPending || d.deletePending);
-			});
-
-		row.select('.srcDPID')
-			.text(function (d) {
-				if (d) {
-					return d.srcDpid;
-				}
-			});
-
-		row.select('.dstDPID')
-			.text(function (d) {
-				if (d) {
-					return d.dstDpid;
-				}
-			});
-	}
-
-	var flows = d3.select('#selectedFlows')
-		.selectAll('.selectedFlow')
-		.data(selectedFlows);
-
-	flows.enter()
-		.append('div')
-		.classed('selectedFlow', true)
-		.each(rowEnter);
-
-	flows.each(rowUpdate);
-
-	flows.exit().remove();
-}
-
-// TODO: cancel the interval when the flow is desel
-function startIPerfForFlow(flow) {
-	var duration = 10000; // seconds
-	var interval = 100; // ms. this is defined by the server
-	var updateRate = 2000; // ms
-	var pointsToDisplay = 1000;
-
-	function makePoints() {
-		var pts = [];
-		var i;
-		for (i=0; i < pointsToDisplay; ++i) {
-			var sample = flow.iperfData.samples[i];
-			var height = 30 * sample/1000000;
-			if (height > 30)
-				height = 30;
-			pts.push({
-				x: i * 1000/(pointsToDisplay-1),
-				y: 32 - height
-			})
-		}
-		return pts;
-	}
-
-	if (flow.flowId) {
-		console.log('starting iperf for: ' + flow.flowId);
-		startIPerf(flow, duration, updateRate/interval);
-		flow.iperfDisplayInterval = setInterval(function () {
-			if (flow.iperfData) {
-				while (flow.iperfData.samples.length < pointsToDisplay) {
-					flow.iperfData.samples.push(0);
-				}
-				var iperfPath = d3.select(document.getElementById(makeSelectedFlowKey(flow))).select('path');
-				iperfPath.attr('d', line(makePoints()));
-				flow.iperfData.samples.shift();
-			}
-
-
-		}, interval);
-		flow.iperfFetchInterval = setInterval(function () {
-			getIPerfData(flow, function (data) {
-				try {
-					if (!flow.iperfData) {
-						flow.iperfData = {
-							samples: []
-						};
-						var i;
-						for (i = 0; i < pointsToDisplay; ++i) {
-							flow.iperfData.samples.push(0);
-						}
-					}
-
-					var iperfData = JSON.parse(data);
-
-//				console.log(iperfData.timestamp);
-
-					// if the data is fresh
-					if (flow.iperfData.timestamp && iperfData.timestamp != flow.iperfData.timestamp) {
-
-						while (flow.iperfData.samples.length > pointsToDisplay + iperfData.samples.length) {
-							flow.iperfData.samples.shift();
-						}
-
-						iperfData.samples.forEach(function (s) {
-							flow.iperfData.samples.push(s);
-						});
-					}
-					flow.iperfData.timestamp = iperfData.timestamp;
-				} catch (e) {
-					console.log('bad iperf data: ' + data);
-				}
-//				console.log(data);
-			});
-		}, updateRate/2); // over sample to avoid gaps
-	}
-}
-
-function updateSelectedFlows() {
-	// make sure that all of the selected flows are either
-	// 1) valid (meaning they are in the latest list of flows)
-	// 2) pending
-	if (model) {
-		var flowMap = {};
-		model.flows.forEach(function (flow) {
-			flowMap[makeFlowKey(flow)] = flow;
-		});
-
-		var newSelectedFlows = [];
-		selectedFlows.forEach(function (flow) {
-			if (flow) {
-				var liveFlow = flowMap[makeFlowKey(flow)];
-				if (liveFlow) {
-					newSelectedFlows.push(liveFlow);
-					liveFlow.deletePending = flow.deletePending;
-					liveFlow.iperfFetchInterval = flow.iperfFetchInterval;
-					liveFlow.iperfDisplayInterval = flow.iperfDisplayInterval;
-				} else if (flow.createPending) {
-					newSelectedFlows.push(flow);
-				} else if (hasIPerf(flow)) {
-					clearIPerf(flow);
-				}
-			}
-		});
-		selectedFlows = newSelectedFlows;
-	}
-	selectedFlows.forEach(function (flow) {
-		if (!hasIPerf(flow)) {
-			startIPerfForFlow(flow);
-		}
-	});
-	while (selectedFlows.length < 3) {
-		selectedFlows.push(null);
-	}
-
-	updateSelectedFlowsTable();
-	updateSelectedFlowsTopology();
-}
-
-function selectFlow(flow) {
-	var flowKey = makeFlowKey(flow);
-	var alreadySelected = false;
-	selectedFlows.forEach(function (f) {
-		if (f && makeFlowKey(f) === flowKey) {
-			alreadySelected = true;
-		}
-	});
-
-	if (!alreadySelected) {
-		selectedFlows.unshift(flow);
-		selectedFlows = selectedFlows.slice(0, 3);
-		updateSelectedFlows();
-	}
-}
-
-function hasIPerf(flow) {
-	return flow && flow.iperfFetchInterval;
-}
-
-function clearIPerf(flow) {
-	console.log('clearing iperf interval for: ' + flow.flowId);
-	clearInterval(flow.iperfFetchInterval);
-	delete flow.iperfFetchInterval;
-	clearInterval(flow.iperfDisplayInterval);
-	delete flow.iperfDisplayInterval;
-	delete flow.iperfData;
-}
-
-function deselectFlow(flow, ifCreatePending) {
-	var flowKey = makeFlowKey(flow);
-	var newSelectedFlows = [];
-	selectedFlows.forEach(function (flow) {
-		if (!flow ||
-				flowKey !== makeFlowKey(flow) ||
-				flowKey === makeFlowKey(flow) && ifCreatePending && !flow.createPending ) {
-			newSelectedFlows.push(flow);
-		} else {
-			if (hasIPerf(flow)) {
-				clearIPerf(flow);
-			}
-		}
-	});
-	selectedFlows = newSelectedFlows;
-	while (selectedFlows.length < 3) {
-		selectedFlows.push(null);
-	}
-
-	updateSelectedFlows();
-}
-
-function deselectFlowIfCreatePending(flow) {
-	deselectFlow(flow, true);
-}
-
-function showFlowChooser() {
-	function rowEnter(d) {
-		var row = d3.select(this);
-
-		row.append('div')
-			.classed('black-eye', true).
-			on('click', function () {
-				selectFlow(d);
-			});
-
-		row.append('div')
-			.classed('flowId', true)
-			.text(function (d) {
-				return d.flowId;
-			});
-
-		row.append('div')
-			.classed('srcDPID', true)
-			.text(function (d) {
-				return d.srcDpid;
-			});
-
-
-		row.append('div')
-			.classed('dstDPID', true)
-			.text(function (d) {
-				return d.dstDpid;
-			});
-
-	}
-
-	var flows = d3.select('#flowChooser')
-		.append('div')
-		.style('pointer-events', 'auto')
-		.selectAll('.selectedFlow')
-		.data(model.flows)
-		.enter()
-		.append('div')
-		.classed('selectedFlow', true)
-		.each(rowEnter);
-
-	setTimeout(function () {
-		d3.select(document.body).on('click', function () {
-			d3.select('#flowChooser').html('');
-			d3.select(document.body).on('click', null);
-		});
-	}, 0);
-}
-
-
-
-function updateHeader(model) {
-	d3.select('#lastUpdate').text(new Date());
-	d3.select('#activeSwitches').text(model.edgeSwitches.length + model.aggregationSwitches.length + model.coreSwitches.length);
-	d3.select('#activeFlows').text(model.flows.length);
-}
-
-function toRadians (angle) {
-  return angle * (Math.PI / 180);
-}
-
-var widths = {
-	edge: 6,
-	aggregation: 12,
-	core: 18
-}
-
-function createRingsFromModel(model) {
-	var rings = [{
-		radius: 3,
-		width: widths.edge,
-		switches: model.edgeSwitches,
-		className: 'edge',
-		angles: []
-	}, {
-		radius: 2.25,
-		width: widths.aggregation,
-		switches: model.aggregationSwitches,
-		className: 'aggregation',
-		angles: []
-	}, {
-		radius: 0.75,
-		width: widths.core,
-		switches: model.coreSwitches,
-		className: 'core',
-		angles: []
-	}];
-
-
-	var aggRanges = {};
-
-	// arrange edge switches at equal increments
-	var k = 360 / rings[0].switches.length;
-	rings[0].switches.forEach(function (s, i) {
-		var angle = k * i;
-
-		rings[0].angles[i] = angle;
-
-		// record the angle for the agg switch layout
-		var dpid = s.dpid.split(':');
-		dpid[7] = '01'; // the last component of the agg switch is always '01'
-		var aggdpid = dpid.join(':');
-		var aggRange = aggRanges[aggdpid];
-		if (!aggRange) {
-			aggRange = aggRanges[aggdpid] = {};
-			aggRange.min = aggRange.max = angle;
-		} else {
-			aggRange.max = angle;
-		}
-	});
-
-	// arrange aggregation switches to "fan out" to edge switches
-	k = 360 / rings[1].switches.length;
-	rings[1].switches.forEach(function (s, i) {
-//		rings[1].angles[i] = k * i;
-		var range = aggRanges[s.dpid];
-
-		rings[1].angles[i] = (range.min + range.max)/2;
-	});
-
-	// find the association between core switches and aggregation switches
-	var aggregationSwitchMap = {};
-	model.aggregationSwitches.forEach(function (s, i) {
-		aggregationSwitchMap[s.dpid] = i;
-	});
-
-	// put core switches next to linked aggregation switches
-	k = 360 / rings[2].switches.length;
-	rings[2].switches.forEach(function (s, i) {
-//		rings[2].angles[i] = k * i;
-		var associatedAggregationSwitches = model.configuration.association[s.dpid];
-		// TODO: go between if there are multiple
-		var index = aggregationSwitchMap[associatedAggregationSwitches[0]];
-
-		rings[2].angles[i] = rings[1].angles[index];
-	});
-
-	// TODO: construct this form initially rather than converting. it works better because
-	// it allows binding by dpid
-	var testRings = [];
-	rings.forEach(function (ring) {
-		var testRing = [];
-		ring.switches.forEach(function (s, i) {
-			var testSwitch = {
-				dpid: s.dpid,
-				state: s.state,
-				radius: ring.radius,
-				width: ring.width,
-				className: ring.className,
-				angle: ring.angles[i],
-				controller: s.controller
-			};
-			testRing.push(testSwitch);
-		});
-
-
-		testRings.push(testRing);
-	});
-
-
-//	return rings;
-	return testRings;
-}
-
-function makeLinkKey(link) {
-	return link['src-switch'] + '=>' + link['dst-switch'];
-}
-
-function makeFlowKey(flow) {
-	return flow.srcDpid + '=>' + flow.dstDpid;
-}
-
-function makeSelectedFlowKey(flow) {
-	return 'S' + makeFlowKey(flow);
-}
-
-function createLinkMap(links) {
-	var linkMap = {};
-	links.forEach(function (link) {
-		var srcDPID = link['src-switch'];
-		var dstDPID = link['dst-switch'];
-
-		var srcMap = linkMap[srcDPID] || {};
-
-		srcMap[dstDPID] = link;
-
-		linkMap[srcDPID]  = srcMap;
-	});
-	return linkMap;
-}
-
-// removes links from the pending list that are now in the model
-function reconcilePendingLinks(model) {
-	var links = [];
-	model.links.forEach(function (link) {
-		links.push(link);
-		delete pendingLinks[makeLinkKey(link)]
-	})
-	var linkId;
-	for (linkId in pendingLinks) {
-		links.push(pendingLinks[linkId]);
-	}
-	return links
-}
-
-updateTopology = function() {
-
-	// DRAW THE SWITCHES
-	var rings = svg.selectAll('.ring').data(createRingsFromModel(model));
-
-	var links = reconcilePendingLinks(model);
-	var linkMap = createLinkMap(links);
-
-	function mouseOverSwitch(data) {
-
-		d3.event.preventDefault();
-
-		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
-
-		if (data.highlighted) {
-			return;
-		}
-
-		// only highlight valid link or flow destination by checking for class of existing highlighted circle
-		var highlighted = svg.selectAll('.highlight')[0];
-		if (highlighted.length == 1) {
-			var s = d3.select(highlighted[0]).select('circle');
-			// only allow links
-			// 	edge->edge (flow)
-			//  aggregation->core
-			//	core->core
-			if (data.className == 'edge' && !s.classed('edge') ||
-				data.className == 'core' && !s.classed('core') && !s.classed('aggregation') ||
-				data.className == 'aggregation' && !s.classed('core')) {
-				return;
-			}
-
-			// don't highlight if there's already a link or flow
-			// var map = linkMap[data.dpid];
-			// console.log(map);
-			// console.log(s.data()[0].dpid);
-			// console.log(map[s.data()[0].dpid]);
-			// if (map && map[s.data()[0].dpid]) {
-			// 	return;
-			// }
-
-			// the second highlighted switch is the target for a link or flow
-			data.target = true;
-		}
-
-		var node = d3.select(document.getElementById(data.dpid));
-		node.classed('highlight', true).select('circle').transition().duration(100).attr("r", widths.core);
-		data.highlighted = true;
-		node.moveToFront();
-	}
-
-	function mouseOutSwitch(data) {
-		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
-
-		if (data.mouseDown)
-			return;
-
-		var node = d3.select(document.getElementById(data.dpid));
-		node.classed('highlight', false).select('circle').transition().duration(100).attr("r", widths[data.className]);
-		data.highlighted = false;
-		data.target = false;
-	}
-
-	function mouseDownSwitch(data) {
-		mouseOverSwitch(data);
-		data.mouseDown = true;
-		d3.select('#topology').classed('linking', true);
-
-		d3.select('svg')
-			.append('svg:path')
-			.attr('id', 'linkVector')
-			.attr('d', function () {
-				var s = d3.select(document.getElementById(data.dpid));
-
-				var pt = document.querySelector('svg').createSVGPoint();
-				pt.x = s.attr('x');
-				pt.y = s.attr('y');
-				pt = pt.matrixTransform(s[0][0].getCTM());
-
-				return line([pt, pt]);
-			});
-
-
-		if (data.className === 'core') {
-			d3.selectAll('.edge').classed('nodrop', true);
-		}
-		if (data.className === 'edge') {
-			d3.selectAll('.core').classed('nodrop', true);
-			d3.selectAll('.aggregation').classed('nodrop', true);
-		}
-		if (data.className === 'aggregation') {
-			d3.selectAll('.edge').classed('nodrop', true);
-			d3.selectAll('.aggregation').classed('nodrop', true);
-		}
-	}
-
-	function mouseUpSwitch(data) {
-		if (data.mouseDown) {
-			data.mouseDown = false;
-			d3.select('#topology').classed('linking', false);
-			d3.event.stopPropagation();
-			d3.selectAll('.nodrop').classed('nodrop', false);
-		}
-	}
-
-	function doubleClickSwitch(data) {
-		var circle = d3.select(document.getElementById(data.dpid)).select('circle');
-		if (data.state == 'ACTIVE') {
-			var prompt = 'Deactivate ' + data.dpid + '?';
-			if (confirm(prompt)) {
-				switchDown(data);
-				setPending(circle);
-			}
-		} else {
-			var prompt = 'Activate ' + data.dpid + '?';
-			if (confirm(prompt)) {
-				switchUp(data);
-				setPending(circle);
-			}
-		}
-	}
-
-	function ringEnter(data, i) {
-		if (!data.length) {
-			return;
-		}
-
-		// create the nodes
-		var nodes = d3.select(this).selectAll("g")
-			.data(data, function (data) {
-				return data.dpid;
-			})
-			.enter().append("svg:g")
-			.attr("id", function (data, i) {
-				return data.dpid;
-			})
-			.attr("transform", function(data, i) {
-				return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
-			});
-
-		// add the cirles representing the switches
-		nodes.append("svg:circle")
-			.attr("transform", function(data, i) {
-				var m = document.querySelector('#viewbox').getTransformToElement().inverse();
-				if (data.scale) {
-					m = m.scale(data.scale);
-				}
-				return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
-			})
-			.attr("x", function (data) {
-				return -data.width / 2;
-			})
-			.attr("y", function (data) {
-				return -data.width / 2;
-			})
-			.attr("r", function (data) {
-				return data.width;
-			});
-
-		// setup the mouseover behaviors
-		nodes.on('mouseover', mouseOverSwitch);
-		nodes.on('mouseout', mouseOutSwitch);
-		nodes.on('mouseup', mouseUpSwitch);
-		nodes.on('mousedown', mouseDownSwitch);
-
-		// only do switch up/down for core switches
-		if (i == 2) {
-			nodes.on('dblclick', doubleClickSwitch);
-		}
-	}
-
-	// append switches
-	rings.enter().append("svg:g")
-		.attr("class", "ring")
-		.each(ringEnter);
-
-
-	function ringUpdate(data, i) {
-		var nodes = d3.select(this).selectAll("g")
-			.data(data, function (data) {
-				return data.dpid;
-			});
-		nodes.select('circle')
-			.each(function (data) {
-				// if there's a pending state changed and then the state changes, clear the pending class
-				var circle = d3.select(this);
-				if (data.state === 'ACTIVE' && circle.classed('inactive') ||
-					data.state === 'INACTIVE' && circle.classed('active')) {
-					circle.classed('pending', false);
-				}
-			})
-			.attr('class', function (data)  {
-				if (data.state === 'ACTIVE' && data.controller) {
-					return data.className + ' active ' + controllerColorMap[data.controller];
-				} else {
-					return data.className + ' inactive ' + 'colorInactive';
-				}
-			});
-	}
-
-	// update  switches
-	rings.each(ringUpdate);
-
-
-	// Now setup the labels
-	// This is done separately because SVG draws in node order and we want the labels
-	// always on top
-	var labelRings = svg.selectAll('.labelRing').data(createRingsFromModel(model));
-
-	d3.select(document.body).on('mousemove', function () {
-		if (!d3.select('#topology').classed('linking')) {
-			return;
-		}
-		var linkVector = document.getElementById('linkVector');
-		if (!linkVector) {
-			return;
-		}
-		linkVector = d3.select(linkVector);
-
-		var highlighted = svg.selectAll('.highlight')[0];
-		var s1 = null, s2 = null;
-		if (highlighted.length > 1) {
-			var s1 = d3.select(highlighted[0]);
-			var s2 = d3.select(highlighted[1]);
-
-		} else if (highlighted.length > 0) {
-			var s1 = d3.select(highlighted[0]);
-		}
-		var src = s1;
-		if (s2 && !s2.data()[0].target) {
-			src = s2;
-		}
-		if (src) {
-			linkVector.attr('d', function () {
-					var srcPt = document.querySelector('svg').createSVGPoint();
-					srcPt.x = src.attr('x');
-					srcPt.y = src.attr('y');
-					srcPt = srcPt.matrixTransform(src[0][0].getCTM());
-
-					var svg = document.getElementById('topology');
-					var mouse = d3.mouse(viewbox);
-					var dstPt = document.querySelector('svg').createSVGPoint();
-					dstPt.x = mouse[0];
-					dstPt.y = mouse[1];
-					dstPt = dstPt.matrixTransform(viewbox.getCTM());
-
-					return line([srcPt, dstPt]);
-				});
-		}
-	});
-
-	d3.select(document.body).on('mouseup', function () {
-		function clearHighlight() {
-			svg.selectAll('circle').each(function (data) {
-				data.mouseDown = false;
-				d3.select('#topology').classed('linking', false);
-				mouseOutSwitch(data);
-			});
-			d3.select('#linkVector').remove();
-		};
-
-		d3.selectAll('.nodrop').classed('nodrop', false);
-
-		function removeLink(link) {
-			var path1 = document.getElementById(link['src-switch'] + '=>' + link['dst-switch']);
-			var path2 = document.getElementById(link['dst-switch'] + '=>' + link['src-switch']);
-
-			if (path1) {
-				setPending(d3.select(path1));
-			}
-			if (path2) {
-				setPending(d3.select(path2));
-			}
-
-			linkDown(link);
-		}
-
-
-		var highlighted = svg.selectAll('.highlight')[0];
-		if (highlighted.length == 2) {
-			var s1Data = highlighted[0].__data__;
-			var s2Data = highlighted[1].__data__;
-
-			var srcData, dstData;
-			if (s1Data.target) {
-				dstData = s1Data;
-				srcData = s2Data;
-			} else {
-				dstData = s2Data;
-				srcData = s1Data;
-			}
-
-			if (s1Data.className == 'edge' && s2Data.className == 'edge') {
-				var prompt = 'Create flow from ' + srcData.dpid + ' to ' + dstData.dpid + '?';
-				if (confirm(prompt)) {
-					addFlow(srcData, dstData);
-
-					var flow = {
-						dataPath: {
-							srcPort: {
-								dpid: {
-									value: srcData.dpid
-								}
-							},
-							dstPort: {
-								dpid: {
-									value: dstData.dpid
-								}
-							}
-						},
-					        srcDpid: srcData.dpid,
-					        dstDpid: dstData.dpid,
-						createPending: true
-					};
-
-					selectFlow(flow);
-
-					setTimeout(function () {
-						deselectFlowIfCreatePending(flow);
-					}, pendingTimeout);
-				}
-			} else {
-				var map = linkMap[srcData.dpid];
-				if (map && map[dstData.dpid]) {
-					var prompt = 'Remove link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
-					if (confirm(prompt)) {
-						removeLink(map[dstData.dpid]);
-					}
-				} else {
-					map = linkMap[dstData.dpid];
-					if (map && map[srcData.dpid]) {
-						var prompt = 'Remove link between ' + dstData.dpid + ' and ' + srcData.dpid + '?';
-						if (confirm(prompt)) {
-							removeLink(map[srcData.dpid]);
-						}
-					} else {
-						var prompt = 'Create link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
-						if (confirm(prompt)) {
-							var link1 = {
-								'src-switch': srcData.dpid,
-								'src-port': 1,
-								'dst-switch': dstData.dpid,
-								'dst-port': 1,
-								pending: true
-							};
-							pendingLinks[makeLinkKey(link1)] = link1;
-							var link2 = {
-								'src-switch': dstData.dpid,
-								'src-port': 1,
-								'dst-switch': srcData.dpid,
-								'dst-port': 1,
-								pending: true
-							};
-							pendingLinks[makeLinkKey(link2)] = link2;
-							updateTopology();
-
-							linkUp(link1);
-
-							// remove the pending links after 10s
-							setTimeout(function () {
-								delete pendingLinks[makeLinkKey(link1)];
-								delete pendingLinks[makeLinkKey(link2)];
-
-								updateTopology();
-							}, pendingTimeout);
-						}
-					}
-				}
-			}
-
-			clearHighlight();
-		} else {
-			clearHighlight();
-		}
-
-	});
-
-	function labelRingEnter(data) {
-		if (!data.length) {
-			return;
-		}
-
-		// create the nodes
-		var nodes = d3.select(this).selectAll("g")
-			.data(data, function (data) {
-				return data.dpid;
-			})
-			.enter().append("svg:g")
-			.classed('nolabel', true)
-			.attr("id", function (data) {
-				return data.dpid + '-label';
-			})
-			.attr("transform", function(data, i) {
-				return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
-			})
-
-		// add the text nodes which show on mouse over
-		nodes.append("svg:text")
-				.text(function (data) {return data.dpid;})
-				.attr("x", function (data) {
-					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
-						if (data.className == 'edge') {
-							return - data.width*3 - 4;
-						} else {
-							return - data.width - 4;
-						}
-					} else {
-						if (data.className == 'edge') {
-							return data.width*3 + 4;
-						} else {
-							return data.width + 4;
-						}
-					}
-				})
-				.attr("y", function (data) {
-					var y;
-					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
-						if (data.className == 'edge') {
-							y = data.width*3/2 + 4;
-						} else {
-							y = data.width/2 + 4;
-						}
-					} else {
-						if (data.className == 'edge') {
-							y = data.width*3/2 + 4;
-						} else {
-							y = data.width/2 + 4;
-						}
-					}
-					return y - 6;
-				})
-				.attr("text-anchor", function (data) {
-					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
-						return "end";
-					} else {
-						return "start";
-					}
-				})
-				.attr("transform", function(data) {
-					var m = document.querySelector('#viewbox').getTransformToElement().inverse();
-					if (data.scale) {
-						m = m.scale(data.scale);
-					}
-					return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
-				})
-	}
-
-	labelRings.enter().append("svg:g")
-		.attr("class", "textRing")
-		.each(labelRingEnter);
-
-	// switches should not change during operation of the ui so no
-	// rings.exit()
-
-
-	// DRAW THE LINKS
-
-	// key on link dpids since these will come/go during demo
-	var links = d3.select('svg').selectAll('.link').data(links, function (d) {
-			return d['src-switch']+'->'+d['dst-switch'];
-	});
-
-	// add new links
-	links.enter().append("svg:path")
-	.attr("class", "link");
-
-	links.attr('id', function (d) {
-			return makeLinkKey(d);
-		})
-		.attr("d", function (d) {
-			var src = d3.select(document.getElementById(d['src-switch']));
-			var dst = d3.select(document.getElementById(d['dst-switch']));
-
-			var srcPt = document.querySelector('svg').createSVGPoint();
-			srcPt.x = src.attr('x');
-			srcPt.y = src.attr('y');
-			srcPt = srcPt.matrixTransform(src[0][0].getCTM());
-
-			var dstPt = document.querySelector('svg').createSVGPoint();
-			dstPt.x = dst.attr('x');
-			dstPt.y = dst.attr('y');
-			dstPt = dstPt.matrixTransform(dst[0][0].getCTM());
-
-			var midPt = document.querySelector('svg').createSVGPoint();
-			midPt.x = (srcPt.x + dstPt.x)/2;
-			midPt.y = (srcPt.y + dstPt.y)/2;
-
-			return line([srcPt, midPt, dstPt]);
-		})
-		.attr("marker-mid", function(d) { return "url(#arrow)"; })
-		.classed('pending', function (d) {
-			return d.pending;
-		});
-
-
-	// remove old links
-	links.exit().remove();
-}
-
-function updateControllers() {
-	var controllers = d3.select('#controllerList').selectAll('.controller').data(model.controllers);
-	controllers.enter().append('div')
-		.each(function (c) {
-			controllerColorMap[c] = colors.pop();
-			d3.select(document.body).classed(controllerColorMap[c] + '-selected', true);
-		})
-		.text(function (d) {
-			return d;
-		})
-		.append('div')
-		.attr('class', 'black-eye');
-
-	controllers.attr('class', function (d) {
-			var color = 'colorInactive';
-			if (model.activeControllers.indexOf(d) != -1) {
-				color = controllerColorMap[d];
-			}
-			var className = 'controller ' + color;
-			return className;
-		});
-
-	// this should never be needed
-	// controllers.exit().remove();
-
-	controllers.on('dblclick', function (c) {
-		if (model.activeControllers.indexOf(c) != -1) {
-			var prompt = 'Dectivate ' + c + '?';
-			if (confirm(prompt)) {
-				controllerDown(c);
-				setPending(d3.select(this));
-			};
-		} else {
-			var prompt = 'Activate ' + c + '?';
-			if (confirm(prompt)) {
-				controllerUp(c);
-				setPending(d3.select(this));
-			};
-		}
-	});
-
-	controllers.select('.black-eye').on('click', function (c) {
-		var allSelected = true;
-		for (var key in controllerColorMap) {
-			if (!d3.select(document.body).classed(controllerColorMap[key] + '-selected')) {
-				allSelected = false;
-				break;
-			}
-		}
-		if (allSelected) {
-			for (var key in controllerColorMap) {
-				d3.select(document.body).classed(controllerColorMap[key] + '-selected', key == c)
-			}
-		} else {
-			for (var key in controllerColorMap) {
-				d3.select(document.body).classed(controllerColorMap[key] + '-selected', true)
-			}
-		}
-
-		// var selected = d3.select(document.body).classed(controllerColorMap[c] + '-selected');
-		// d3.select(document.body).classed(controllerColorMap[c] + '-selected', !selected);
-	});
-
-
-}
 
-var modelString;
 function sync(svg) {
 	var d = Date.now();
+
 	updateModel(function (newModel) {
 //		console.log('Update time: ' + (Date.now() - d)/1000 + 's');
 
@@ -1222,6 +25,8 @@
 			}
 
 			updateHeader(newModel);
+
+			d3.select('#contents').style('visibility', 'visible');
 		}
 
 		// do it again in 1s
@@ -1231,27 +36,7 @@
 	});
 }
 
-svg = createTopologyView();
-
-
-d3.xml("assets/map.svg", "image/svg+xml", function(xml) {
-  var importedNode = document.importNode(xml.documentElement, true);
-  var paths = importedNode.querySelectorAll('path');
-  var i;
-  for (i=0; i < paths.length; i+=1) {
-  	svg.append('svg:path')
-  		.attr('class', 'state')
-  		.attr('d', d3.select(paths.item(i)).attr('d'))
-  		.attr('transform', 'translate(-500 -500)scale(1 1.7)')
-  }
-});
-
-updateSelectedFlows();
-
-d3.select('#showFlowChooser').on('click', function () {
-	showFlowChooser();
-});
-
+appInit();
 
 // workaround for Chrome v25 bug
 // if executed immediately, the view box transform logic doesn't work properly
diff --git a/web/ons-demo/js/constants.js b/web/ons-demo/js/constants.js
new file mode 100644
index 0000000..7431bba
--- /dev/null
+++ b/web/ons-demo/js/constants.js
@@ -0,0 +1,21 @@
+/***************************************************************************************************
+timeout used by controller functions. after the timeout expires the "pending" action
+is removed and the topology view is whatever is reported by the API
+***************************************************************************************************/
+var pendingTimeout = 30000;
+
+/***************************************************************************************************
+CSS names for the pallette of colors used by the topology view
+***************************************************************************************************/
+var colors = [
+	'color1',
+	'color2',
+	'color3',
+	'color4',
+	'color7',
+	'color8',
+	'color9',
+//	'color11',
+	'color12'
+];
+colors.reverse();
\ No newline at end of file
diff --git a/web/ons-demo/js/controllers.js b/web/ons-demo/js/controllers.js
new file mode 100644
index 0000000..75f2689
--- /dev/null
+++ b/web/ons-demo/js/controllers.js
@@ -0,0 +1,65 @@
+function updateControllers() {
+	var controllers = d3.select('#controllerList').selectAll('.controller').data(model.controllers);
+	controllers.enter().append('div')
+		.each(function (c) {
+			controllerColorMap[c] = colors.pop();
+			d3.select(document.body).classed(controllerColorMap[c] + '-selected', true);
+		})
+		.text(function (d) {
+			return d;
+		})
+		.append('div')
+		.attr('class', 'black-eye');
+
+	controllers.attr('class', function (d) {
+			var color = 'colorInactive';
+			if (model.activeControllers.indexOf(d) != -1) {
+				color = controllerColorMap[d];
+			}
+			var className = 'controller ' + color;
+			return className;
+		});
+
+	// this should never be needed
+	// controllers.exit().remove();
+
+	controllers.on('dblclick', function (c) {
+		if (model.activeControllers.indexOf(c) != -1) {
+			var prompt = 'Dectivate ' + c + '?';
+			if (confirm(prompt)) {
+				controllerDown(c);
+				setPending(d3.select(this));
+			};
+		} else {
+			var prompt = 'Activate ' + c + '?';
+			if (confirm(prompt)) {
+				controllerUp(c);
+				setPending(d3.select(this));
+			};
+		}
+	});
+
+	controllers.select('.black-eye').on('click', function (c) {
+		var allSelected = true;
+		for (var key in controllerColorMap) {
+			if (!d3.select(document.body).classed(controllerColorMap[key] + '-selected')) {
+				allSelected = false;
+				break;
+			}
+		}
+		if (allSelected) {
+			for (var key in controllerColorMap) {
+				d3.select(document.body).classed(controllerColorMap[key] + '-selected', key == c)
+			}
+		} else {
+			for (var key in controllerColorMap) {
+				d3.select(document.body).classed(controllerColorMap[key] + '-selected', true)
+			}
+		}
+
+		// var selected = d3.select(document.body).classed(controllerColorMap[c] + '-selected');
+		// d3.select(document.body).classed(controllerColorMap[c] + '-selected', !selected);
+	});
+
+
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/debug.js b/web/ons-demo/js/debug.js
new file mode 100644
index 0000000..7b66912
--- /dev/null
+++ b/web/ons-demo/js/debug.js
@@ -0,0 +1,12 @@
+/***************************************************************************************************
+find the links that include the switch with this dpid
+***************************************************************************************************/
+function debug_findlink(model, dpid) {
+	var links = [];
+	model.links.forEach(function (link) {
+		if (link['src-switch'] == dpid || link['dst-switch'] == dpid) {
+			links.push(link);
+		}
+	});
+	return links;
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/flows.js b/web/ons-demo/js/flows.js
new file mode 100644
index 0000000..29fc4fa
--- /dev/null
+++ b/web/ons-demo/js/flows.js
@@ -0,0 +1,408 @@
+function updateSelectedFlowsTopology() {
+	// DRAW THE FLOWS
+	var topologyFlows = [];
+	selectedFlows.forEach(function (flow) {
+		if (flow) {
+			topologyFlows.push(flow);
+		}
+	});
+
+	var flows = d3.select('svg').selectAll('.flow').data(topologyFlows);
+
+	flows.enter().append("svg:path").attr('class', 'flow')
+		.attr('stroke-dasharray', '4, 10')
+		.append('svg:animate')
+		.attr('attributeName', 'stroke-dashoffset')
+		.attr('attributeType', 'xml')
+		.attr('from', '500')
+		.attr('to', '-500')
+		.attr('dur', '20s')
+		.attr('repeatCount', 'indefinite');
+
+	flows.exit().remove();
+
+	flows.attr('d', function (d) {
+			if (!d) {
+				return;
+			}
+			var pts = [];
+			if (!d.dataPath.flowEntries) {
+				// create a temporary vector to indicate the pending flow
+				var s1 = d3.select(document.getElementById(d.srcDpid));
+				var s2 = d3.select(document.getElementById(d.dstDpid));
+
+				var pt1 = document.querySelector('svg').createSVGPoint();
+				pt1.x = s1.attr('x');
+				pt1.y = s1.attr('y');
+				pt1 = pt1.matrixTransform(s1[0][0].getCTM());
+				pts.push(pt1);
+
+				var pt2 = document.querySelector('svg').createSVGPoint();
+				pt2.x = s2.attr('x');
+				pt2.y = s2.attr('y');
+				pt2 = pt2.matrixTransform(s2[0][0].getCTM());
+				pts.push(pt2);
+
+			} else {
+				d.dataPath.flowEntries.forEach(function (flowEntry) {
+					var s = d3.select(document.getElementById(flowEntry.dpid.value));
+					// s[0] is null if the flow entry refers to a non-existent switch
+					if (s[0][0]) {
+						var pt = document.querySelector('svg').createSVGPoint();
+						pt.x = s.attr('x');
+						pt.y = s.attr('y');
+						pt = pt.matrixTransform(s[0][0].getCTM());
+						pts.push(pt);
+					} else {
+						console.log('flow refers to non-existent switch: ' + flowEntry.dpid.value);
+					}
+				});
+			}
+			if (pts.length) {
+				return line(pts);
+			} else {
+				return "M0,0";
+			}
+		})
+		.attr('id', function (d) {
+			if (d) {
+				return makeFlowKey(d);
+			}
+		})
+		.classed('pending', function (d) {
+			return d && (d.createPending || d.deletePending);
+		});
+
+	// "marching ants"
+	flows.select('animate').attr('from', 500);
+
+}
+
+function updateSelectedFlowsTable() {
+	function rowEnter(d) {
+		var row = d3.select(this);
+		row.append('div').classed('deleteFlow', true);
+		row.append('div').classed('flowId', true);
+		row.append('div').classed('srcDPID', true);
+		row.append('div').classed('dstDPID', true);
+		row.append('div').classed('iperf', true);
+
+		row.select('.iperf')
+			.append('div')
+			.attr('class', 'iperf-container')
+			.append('svg:svg')
+			.attr('viewBox', '0 0 1000 32')
+			.attr('preserveAspectRatio', 'none')
+			.append('svg:g')
+			.append('svg:path')
+			.attr('class', 'iperfdata');
+
+		row.on('mouseover', function (d) {
+			if (d) {
+				var path = document.getElementById(makeFlowKey(d));
+				d3.select(path).classed('highlight', true);
+			}
+		});
+		row.on('mouseout', function (d) {
+			if (d) {
+				var path = document.getElementById(makeFlowKey(d));
+				d3.select(path).classed('highlight', false);
+			}
+		});
+	}
+
+	function rowUpdate(d) {
+		var row = d3.select(this);
+		row.attr('id', function (d) {
+			if (d) {
+				return makeSelectedFlowKey(d);
+			}
+		});
+
+		if (!d || !hasIPerf(d)) {
+			row.select('.iperfdata')
+				.attr('d', 'M0,0');
+		}
+
+		row.select('.deleteFlow').on('click', function () {
+			deselectFlow(d);
+		});
+		row.on('dblclick', function () {
+			if (d) {
+				var prompt = 'Delete flow ' + d.flowId + '?';
+				if (confirm(prompt)) {
+					deleteFlow(d);
+					d.deletePending = true;
+					updateSelectedFlows();
+
+					setTimeout(function () {
+						d.deletePending = false;
+						updateSelectedFlows();
+					}, pendingTimeout)
+				};
+			}
+		});
+
+		row.select('.flowId')
+			.text(function (d) {
+				if (d) {
+					if (d.flowId) {
+						return d.flowId;
+					} else {
+						return '0x--';
+					}
+				}
+			})
+			.classed('pending', function (d) {
+				return d && (d.createPending || d.deletePending);
+			});
+
+		row.select('.srcDPID')
+			.text(function (d) {
+				if (d) {
+					return d.srcDpid;
+				}
+			});
+
+		row.select('.dstDPID')
+			.text(function (d) {
+				if (d) {
+					return d.dstDpid;
+				}
+			});
+	}
+
+	var flows = d3.select('#selectedFlows')
+		.selectAll('.selectedFlow')
+		.data(selectedFlows);
+
+	flows.enter()
+		.append('div')
+		.classed('selectedFlow', true)
+		.each(rowEnter);
+
+	flows.each(rowUpdate);
+
+	flows.exit().remove();
+}
+
+// TODO: cancel the interval when the flow is desel
+function startIPerfForFlow(flow) {
+	var duration = 10000; // seconds
+	var interval = 100; // ms. this is defined by the server
+	var updateRate = 2000; // ms
+	var pointsToDisplay = 1000;
+
+	function makePoints() {
+		var pts = [];
+		var i;
+		for (i=0; i < pointsToDisplay; ++i) {
+			var sample = flow.iperfData.samples[i];
+			var height = 30 * sample/1000000;
+			if (height > 30)
+				height = 30;
+			pts.push({
+				x: i * 1000/(pointsToDisplay-1),
+				y: 32 - height
+			})
+		}
+		return pts;
+	}
+
+	if (flow.flowId) {
+		console.log('starting iperf for: ' + flow.flowId);
+		startIPerf(flow, duration, updateRate/interval);
+		flow.iperfDisplayInterval = setInterval(function () {
+			if (flow.iperfData) {
+				while (flow.iperfData.samples.length < pointsToDisplay) {
+					flow.iperfData.samples.push(0);
+				}
+				var iperfPath = d3.select(document.getElementById(makeSelectedFlowKey(flow))).select('path');
+				iperfPath.attr('d', line(makePoints()));
+				flow.iperfData.samples.shift();
+			}
+
+
+		}, interval);
+		flow.iperfFetchInterval = setInterval(function () {
+			getIPerfData(flow, function (data) {
+				try {
+					if (!flow.iperfData) {
+						flow.iperfData = {
+							samples: []
+						};
+						var i;
+						for (i = 0; i < pointsToDisplay; ++i) {
+							flow.iperfData.samples.push(0);
+						}
+					}
+
+					var iperfData = JSON.parse(data);
+
+//				console.log(iperfData.timestamp);
+
+					// if the data is fresh
+					if (flow.iperfData.timestamp && iperfData.timestamp != flow.iperfData.timestamp) {
+
+						while (flow.iperfData.samples.length > pointsToDisplay + iperfData.samples.length) {
+							flow.iperfData.samples.shift();
+						}
+
+						iperfData.samples.forEach(function (s) {
+							flow.iperfData.samples.push(s);
+						});
+					}
+					flow.iperfData.timestamp = iperfData.timestamp;
+				} catch (e) {
+					console.log('bad iperf data: ' + data);
+				}
+//				console.log(data);
+			});
+		}, updateRate/2); // over sample to avoid gaps
+	}
+}
+
+function updateSelectedFlows() {
+	// make sure that all of the selected flows are either
+	// 1) valid (meaning they are in the latest list of flows)
+	// 2) pending
+	if (model) {
+		var flowMap = {};
+		model.flows.forEach(function (flow) {
+			flowMap[makeFlowKey(flow)] = flow;
+		});
+
+		var newSelectedFlows = [];
+		selectedFlows.forEach(function (flow) {
+			if (flow) {
+				var liveFlow = flowMap[makeFlowKey(flow)];
+				if (liveFlow) {
+					newSelectedFlows.push(liveFlow);
+					liveFlow.deletePending = flow.deletePending;
+					liveFlow.iperfFetchInterval = flow.iperfFetchInterval;
+					liveFlow.iperfDisplayInterval = flow.iperfDisplayInterval;
+				} else if (flow.createPending) {
+					newSelectedFlows.push(flow);
+				} else if (hasIPerf(flow)) {
+					clearIPerf(flow);
+				}
+			}
+		});
+		selectedFlows = newSelectedFlows;
+	}
+	selectedFlows.forEach(function (flow) {
+		if (!hasIPerf(flow)) {
+			startIPerfForFlow(flow);
+		}
+	});
+	while (selectedFlows.length < 3) {
+		selectedFlows.push(null);
+	}
+
+	updateSelectedFlowsTable();
+	updateSelectedFlowsTopology();
+}
+
+function selectFlow(flow) {
+	var flowKey = makeFlowKey(flow);
+	var alreadySelected = false;
+	selectedFlows.forEach(function (f) {
+		if (f && makeFlowKey(f) === flowKey) {
+			alreadySelected = true;
+		}
+	});
+
+	if (!alreadySelected) {
+		selectedFlows.unshift(flow);
+		selectedFlows = selectedFlows.slice(0, 3);
+		updateSelectedFlows();
+	}
+}
+
+function hasIPerf(flow) {
+	return flow && flow.iperfFetchInterval;
+}
+
+function clearIPerf(flow) {
+	console.log('clearing iperf interval for: ' + flow.flowId);
+	clearInterval(flow.iperfFetchInterval);
+	delete flow.iperfFetchInterval;
+	clearInterval(flow.iperfDisplayInterval);
+	delete flow.iperfDisplayInterval;
+	delete flow.iperfData;
+}
+
+function deselectFlow(flow, ifCreatePending) {
+	var flowKey = makeFlowKey(flow);
+	var newSelectedFlows = [];
+	selectedFlows.forEach(function (flow) {
+		if (!flow ||
+				flowKey !== makeFlowKey(flow) ||
+				flowKey === makeFlowKey(flow) && ifCreatePending && !flow.createPending ) {
+			newSelectedFlows.push(flow);
+		} else {
+			if (hasIPerf(flow)) {
+				clearIPerf(flow);
+			}
+		}
+	});
+	selectedFlows = newSelectedFlows;
+	while (selectedFlows.length < 3) {
+		selectedFlows.push(null);
+	}
+
+	updateSelectedFlows();
+}
+
+function deselectFlowIfCreatePending(flow) {
+	deselectFlow(flow, true);
+}
+
+function showFlowChooser() {
+	function rowEnter(d) {
+		var row = d3.select(this);
+
+		row.append('div')
+			.classed('black-eye', true).
+			on('click', function () {
+				selectFlow(d);
+			});
+
+		row.append('div')
+			.classed('flowId', true)
+			.text(function (d) {
+				return d.flowId;
+			});
+
+		row.append('div')
+			.classed('srcDPID', true)
+			.text(function (d) {
+				return d.srcDpid;
+			});
+
+
+		row.append('div')
+			.classed('dstDPID', true)
+			.text(function (d) {
+				return d.dstDpid;
+			});
+
+	}
+
+	var flows = d3.select('#flowChooser')
+		.append('div')
+		.style('pointer-events', 'auto')
+		.selectAll('.selectedFlow')
+		.data(model.flows)
+		.enter()
+		.append('div')
+		.classed('selectedFlow', true)
+		.each(rowEnter);
+
+	setTimeout(function () {
+		d3.select(document.body).on('click', function () {
+			d3.select('#flowChooser').html('');
+			d3.select(document.body).on('click', null);
+		});
+	}, 0);
+}
diff --git a/web/ons-demo/js/forward.js b/web/ons-demo/js/forward.js
new file mode 100644
index 0000000..9c4d03f
--- /dev/null
+++ b/web/ons-demo/js/forward.js
@@ -0,0 +1,5 @@
+/***************************************************************************************************
+forward declarations for functions
+***************************************************************************************************/
+
+var updateTopology;
diff --git a/web/ons-demo/js/globals.js b/web/ons-demo/js/globals.js
new file mode 100644
index 0000000..30f42f2
--- /dev/null
+++ b/web/ons-demo/js/globals.js
@@ -0,0 +1,37 @@
+/***************************************************************************************************
+global variables
+***************************************************************************************************/
+
+
+/***************************************************************************************************
+the latest update to the model
+***************************************************************************************************/
+var model;
+
+/***************************************************************************************************
+cached JSON representation of the model. used to detect model changes and update the UI.
+***************************************************************************************************/
+var modelString;
+
+
+/***************************************************************************************************
+the svg element for the topology view
+***************************************************************************************************/
+var svg;
+
+/***************************************************************************************************
+links that were created in the webui but which have not appeared in the links API response yet
+these timeout after pendingTimeout
+***************************************************************************************************/
+var pendingLinks = {};
+
+/***************************************************************************************************
+the flows that are displayed in the selected flow table
+this may include pending flows which have not appeared in the flows API response yet
+***************************************************************************************************/
+var selectedFlows = [];
+
+/***************************************************************************************************
+a mapping from controller name to color used for color coding the topology and ONOS nodes views
+***************************************************************************************************/
+var controllerColorMap = {};
\ No newline at end of file
diff --git a/web/ons-demo/js/init.js b/web/ons-demo/js/init.js
new file mode 100644
index 0000000..36a899c
--- /dev/null
+++ b/web/ons-demo/js/init.js
@@ -0,0 +1,10 @@
+function appInit() {
+	svg = createTopologyView();
+
+	// populates selected flows with empty rows
+	updateSelectedFlows();
+
+	d3.select('#showFlowChooser').on('click', function () {
+		showFlowChooser();
+	});
+}
diff --git a/web/ons-demo/js/topology.js b/web/ons-demo/js/topology.js
new file mode 100644
index 0000000..b7a9112
--- /dev/null
+++ b/web/ons-demo/js/topology.js
@@ -0,0 +1,675 @@
+/***************************************************************************************************
+functions for creating and interacting with the topology view of the webui
+
+flow related topology is in flows.js
+***************************************************************************************************/
+
+function createLinkMap(links) {
+	var linkMap = {};
+	links.forEach(function (link) {
+		var srcDPID = link['src-switch'];
+		var dstDPID = link['dst-switch'];
+
+		var srcMap = linkMap[srcDPID] || {};
+
+		srcMap[dstDPID] = link;
+
+		linkMap[srcDPID]  = srcMap;
+	});
+	return linkMap;
+}
+
+
+// removes links from the pending list that are now in the model
+function reconcilePendingLinks(model) {
+	var links = [];
+	model.links.forEach(function (link) {
+		links.push(link);
+		delete pendingLinks[makeLinkKey(link)]
+	})
+	var linkId;
+	for (linkId in pendingLinks) {
+		links.push(pendingLinks[linkId]);
+	}
+	return links
+}
+
+
+function createTopologyView() {
+
+	window.addEventListener('resize', function () {
+		// this is too slow. instead detect first resize event and hide the paths that have explicit matrix applied
+		// either that or is it possible to position the paths so they get the automatic transform as well?
+//		updateTopology();
+	});
+
+	var svg = d3.select('#svg-container').append('svg:svg');
+
+	svg.append("svg:defs").append("svg:marker")
+	    .attr("id", "arrow")
+	    .attr("viewBox", "0 -5 10 10")
+	    .attr("refX", -1)
+	    .attr("markerWidth", 5)
+	    .attr("markerHeight", 5)
+	    .attr("orient", "auto")
+	  .append("svg:path")
+	    .attr("d", "M0,-3L10,0L0,3");
+
+	return svg.append('svg:svg').attr('id', 'viewBox').attr('viewBox', '0 0 1000 1000').attr('preserveAspectRatio', 'none').
+			attr('id', 'viewbox').append('svg:g').attr('transform', 'translate(500 500)');
+}
+
+// d3.xml("assets/map.svg", "image/svg+xml", function(xml) {
+//   var importedNode = document.importNode(xml.documentElement, true);
+//   var paths = importedNode.querySelectorAll('path');
+//   var i;
+//   for (i=0; i < paths.length; i+=1) {
+//   	svg.append('svg:path')
+//   		.attr('class', 'state')
+//   		.attr('d', d3.select(paths.item(i)).attr('d'))
+//   		.attr('transform', 'translate(-500 -500)scale(1 1.7)')
+//   }
+// });
+
+
+var widths = {
+	edge: 6,
+	aggregation: 12,
+	core: 18
+}
+
+function createRingsFromModel(model) {
+	var rings = [{
+		radius: 3,
+		width: widths.edge,
+		switches: model.edgeSwitches,
+		className: 'edge',
+		angles: []
+	}, {
+		radius: 2.25,
+		width: widths.aggregation,
+		switches: model.aggregationSwitches,
+		className: 'aggregation',
+		angles: []
+	}, {
+		radius: 0.75,
+		width: widths.core,
+		switches: model.coreSwitches,
+		className: 'core',
+		angles: []
+	}];
+
+
+	var aggRanges = {};
+
+	// arrange edge switches at equal increments
+	var k = 360 / rings[0].switches.length;
+	rings[0].switches.forEach(function (s, i) {
+		var angle = k * i;
+
+		rings[0].angles[i] = angle;
+
+		// record the angle for the agg switch layout
+		var dpid = s.dpid.split(':');
+		dpid[7] = '01'; // the last component of the agg switch is always '01'
+		var aggdpid = dpid.join(':');
+		var aggRange = aggRanges[aggdpid];
+		if (!aggRange) {
+			aggRange = aggRanges[aggdpid] = {};
+			aggRange.min = aggRange.max = angle;
+		} else {
+			aggRange.max = angle;
+		}
+	});
+
+	// arrange aggregation switches to "fan out" to edge switches
+	k = 360 / rings[1].switches.length;
+	rings[1].switches.forEach(function (s, i) {
+//		rings[1].angles[i] = k * i;
+		var range = aggRanges[s.dpid];
+
+		rings[1].angles[i] = (range.min + range.max)/2;
+	});
+
+	// find the association between core switches and aggregation switches
+	var aggregationSwitchMap = {};
+	model.aggregationSwitches.forEach(function (s, i) {
+		aggregationSwitchMap[s.dpid] = i;
+	});
+
+	// put core switches next to linked aggregation switches
+	k = 360 / rings[2].switches.length;
+	rings[2].switches.forEach(function (s, i) {
+//		rings[2].angles[i] = k * i;
+		var associatedAggregationSwitches = model.configuration.association[s.dpid];
+		// TODO: go between if there are multiple
+		var index = aggregationSwitchMap[associatedAggregationSwitches[0]];
+
+		rings[2].angles[i] = rings[1].angles[index];
+	});
+
+	// TODO: construct this form initially rather than converting. it works better because
+	// it allows binding by dpid
+	var testRings = [];
+	rings.forEach(function (ring) {
+		var testRing = [];
+		ring.switches.forEach(function (s, i) {
+			var testSwitch = {
+				dpid: s.dpid,
+				state: s.state,
+				radius: ring.radius,
+				width: ring.width,
+				className: ring.className,
+				angle: ring.angles[i],
+				controller: s.controller
+			};
+			testRing.push(testSwitch);
+		});
+
+
+		testRings.push(testRing);
+	});
+
+
+//	return rings;
+	return testRings;
+}
+
+updateTopology = function() {
+
+	// DRAW THE SWITCHES
+	var rings = svg.selectAll('.ring').data(createRingsFromModel(model));
+
+	var links = reconcilePendingLinks(model);
+	var linkMap = createLinkMap(links);
+
+	function mouseOverSwitch(data) {
+
+		d3.event.preventDefault();
+
+		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
+
+		if (data.highlighted) {
+			return;
+		}
+
+		// only highlight valid link or flow destination by checking for class of existing highlighted circle
+		var highlighted = svg.selectAll('.highlight')[0];
+		if (highlighted.length == 1) {
+			var s = d3.select(highlighted[0]).select('circle');
+			// only allow links
+			// 	edge->edge (flow)
+			//  aggregation->core
+			//	core->core
+			if (data.className == 'edge' && !s.classed('edge') ||
+				data.className == 'core' && !s.classed('core') && !s.classed('aggregation') ||
+				data.className == 'aggregation' && !s.classed('core')) {
+				return;
+			}
+
+			// don't highlight if there's already a link or flow
+			// var map = linkMap[data.dpid];
+			// console.log(map);
+			// console.log(s.data()[0].dpid);
+			// console.log(map[s.data()[0].dpid]);
+			// if (map && map[s.data()[0].dpid]) {
+			// 	return;
+			// }
+
+			// the second highlighted switch is the target for a link or flow
+			data.target = true;
+		}
+
+		var node = d3.select(document.getElementById(data.dpid));
+		node.classed('highlight', true).select('circle').transition().duration(100).attr("r", widths.core);
+		data.highlighted = true;
+		node.moveToFront();
+	}
+
+	function mouseOutSwitch(data) {
+		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
+
+		if (data.mouseDown)
+			return;
+
+		var node = d3.select(document.getElementById(data.dpid));
+		node.classed('highlight', false).select('circle').transition().duration(100).attr("r", widths[data.className]);
+		data.highlighted = false;
+		data.target = false;
+	}
+
+	function mouseDownSwitch(data) {
+		mouseOverSwitch(data);
+		data.mouseDown = true;
+		d3.select('#topology').classed('linking', true);
+
+		d3.select('svg')
+			.append('svg:path')
+			.attr('id', 'linkVector')
+			.attr('d', function () {
+				var s = d3.select(document.getElementById(data.dpid));
+
+				var pt = document.querySelector('svg').createSVGPoint();
+				pt.x = s.attr('x');
+				pt.y = s.attr('y');
+				pt = pt.matrixTransform(s[0][0].getCTM());
+
+				return line([pt, pt]);
+			});
+
+
+		if (data.className === 'core') {
+			d3.selectAll('.edge').classed('nodrop', true);
+		}
+		if (data.className === 'edge') {
+			d3.selectAll('.core').classed('nodrop', true);
+			d3.selectAll('.aggregation').classed('nodrop', true);
+		}
+		if (data.className === 'aggregation') {
+			d3.selectAll('.edge').classed('nodrop', true);
+			d3.selectAll('.aggregation').classed('nodrop', true);
+		}
+	}
+
+	function mouseUpSwitch(data) {
+		if (data.mouseDown) {
+			data.mouseDown = false;
+			d3.select('#topology').classed('linking', false);
+			d3.event.stopPropagation();
+			d3.selectAll('.nodrop').classed('nodrop', false);
+		}
+	}
+
+	function doubleClickSwitch(data) {
+		var circle = d3.select(document.getElementById(data.dpid)).select('circle');
+		if (data.state == 'ACTIVE') {
+			var prompt = 'Deactivate ' + data.dpid + '?';
+			if (confirm(prompt)) {
+				switchDown(data);
+				setPending(circle);
+			}
+		} else {
+			var prompt = 'Activate ' + data.dpid + '?';
+			if (confirm(prompt)) {
+				switchUp(data);
+				setPending(circle);
+			}
+		}
+	}
+
+	function ringEnter(data, i) {
+		if (!data.length) {
+			return;
+		}
+
+		// create the nodes
+		var nodes = d3.select(this).selectAll("g")
+			.data(data, function (data) {
+				return data.dpid;
+			})
+			.enter().append("svg:g")
+			.attr("id", function (data, i) {
+				return data.dpid;
+			})
+			.attr("transform", function(data, i) {
+				return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
+			});
+
+		// add the cirles representing the switches
+		nodes.append("svg:circle")
+			.attr("transform", function(data, i) {
+				var m = document.querySelector('#viewbox').getTransformToElement().inverse();
+				if (data.scale) {
+					m = m.scale(data.scale);
+				}
+				return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
+			})
+			.attr("x", function (data) {
+				return -data.width / 2;
+			})
+			.attr("y", function (data) {
+				return -data.width / 2;
+			})
+			.attr("r", function (data) {
+				return data.width;
+			});
+
+		// setup the mouseover behaviors
+		nodes.on('mouseover', mouseOverSwitch);
+		nodes.on('mouseout', mouseOutSwitch);
+		nodes.on('mouseup', mouseUpSwitch);
+		nodes.on('mousedown', mouseDownSwitch);
+
+		// only do switch up/down for core switches
+		if (i == 2) {
+			nodes.on('dblclick', doubleClickSwitch);
+		}
+	}
+
+	// append switches
+	rings.enter().append("svg:g")
+		.attr("class", "ring")
+		.each(ringEnter);
+
+
+	function ringUpdate(data, i) {
+		var nodes = d3.select(this).selectAll("g")
+			.data(data, function (data) {
+				return data.dpid;
+			});
+		nodes.select('circle')
+			.each(function (data) {
+				// if there's a pending state changed and then the state changes, clear the pending class
+				var circle = d3.select(this);
+				if (data.state === 'ACTIVE' && circle.classed('inactive') ||
+					data.state === 'INACTIVE' && circle.classed('active')) {
+					circle.classed('pending', false);
+				}
+			})
+			.attr('class', function (data)  {
+				if (data.state === 'ACTIVE' && data.controller) {
+					return data.className + ' active ' + controllerColorMap[data.controller];
+				} else {
+					return data.className + ' inactive ' + 'colorInactive';
+				}
+			});
+	}
+
+	// update  switches
+	rings.each(ringUpdate);
+
+
+	// Now setup the labels
+	// This is done separately because SVG draws in node order and we want the labels
+	// always on top
+	var labelRings = svg.selectAll('.labelRing').data(createRingsFromModel(model));
+
+	d3.select(document.body).on('mousemove', function () {
+		if (!d3.select('#topology').classed('linking')) {
+			return;
+		}
+		var linkVector = document.getElementById('linkVector');
+		if (!linkVector) {
+			return;
+		}
+		linkVector = d3.select(linkVector);
+
+		var highlighted = svg.selectAll('.highlight')[0];
+		var s1 = null, s2 = null;
+		if (highlighted.length > 1) {
+			var s1 = d3.select(highlighted[0]);
+			var s2 = d3.select(highlighted[1]);
+
+		} else if (highlighted.length > 0) {
+			var s1 = d3.select(highlighted[0]);
+		}
+		var src = s1;
+		if (s2 && !s2.data()[0].target) {
+			src = s2;
+		}
+		if (src) {
+			linkVector.attr('d', function () {
+					var srcPt = document.querySelector('svg').createSVGPoint();
+					srcPt.x = src.attr('x');
+					srcPt.y = src.attr('y');
+					srcPt = srcPt.matrixTransform(src[0][0].getCTM());
+
+					var svg = document.getElementById('topology');
+					var mouse = d3.mouse(viewbox);
+					var dstPt = document.querySelector('svg').createSVGPoint();
+					dstPt.x = mouse[0];
+					dstPt.y = mouse[1];
+					dstPt = dstPt.matrixTransform(viewbox.getCTM());
+
+					return line([srcPt, dstPt]);
+				});
+		}
+	});
+
+	d3.select(document.body).on('mouseup', function () {
+		function clearHighlight() {
+			svg.selectAll('circle').each(function (data) {
+				data.mouseDown = false;
+				d3.select('#topology').classed('linking', false);
+				mouseOutSwitch(data);
+			});
+			d3.select('#linkVector').remove();
+		};
+
+		d3.selectAll('.nodrop').classed('nodrop', false);
+
+		function removeLink(link) {
+			var path1 = document.getElementById(link['src-switch'] + '=>' + link['dst-switch']);
+			var path2 = document.getElementById(link['dst-switch'] + '=>' + link['src-switch']);
+
+			if (path1) {
+				setPending(d3.select(path1));
+			}
+			if (path2) {
+				setPending(d3.select(path2));
+			}
+
+			linkDown(link);
+		}
+
+
+		var highlighted = svg.selectAll('.highlight')[0];
+		if (highlighted.length == 2) {
+			var s1Data = highlighted[0].__data__;
+			var s2Data = highlighted[1].__data__;
+
+			var srcData, dstData;
+			if (s1Data.target) {
+				dstData = s1Data;
+				srcData = s2Data;
+			} else {
+				dstData = s2Data;
+				srcData = s1Data;
+			}
+
+			if (s1Data.className == 'edge' && s2Data.className == 'edge') {
+				var prompt = 'Create flow from ' + srcData.dpid + ' to ' + dstData.dpid + '?';
+				if (confirm(prompt)) {
+					addFlow(srcData, dstData);
+
+					var flow = {
+						dataPath: {
+							srcPort: {
+								dpid: {
+									value: srcData.dpid
+								}
+							},
+							dstPort: {
+								dpid: {
+									value: dstData.dpid
+								}
+							}
+						},
+					        srcDpid: srcData.dpid,
+					        dstDpid: dstData.dpid,
+						createPending: true
+					};
+
+					selectFlow(flow);
+
+					setTimeout(function () {
+						deselectFlowIfCreatePending(flow);
+					}, pendingTimeout);
+				}
+			} else {
+				var map = linkMap[srcData.dpid];
+				if (map && map[dstData.dpid]) {
+					var prompt = 'Remove link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
+					if (confirm(prompt)) {
+						removeLink(map[dstData.dpid]);
+					}
+				} else {
+					map = linkMap[dstData.dpid];
+					if (map && map[srcData.dpid]) {
+						var prompt = 'Remove link between ' + dstData.dpid + ' and ' + srcData.dpid + '?';
+						if (confirm(prompt)) {
+							removeLink(map[srcData.dpid]);
+						}
+					} else {
+						var prompt = 'Create link between ' + srcData.dpid + ' and ' + dstData.dpid + '?';
+						if (confirm(prompt)) {
+							var link1 = {
+								'src-switch': srcData.dpid,
+								'src-port': 1,
+								'dst-switch': dstData.dpid,
+								'dst-port': 1,
+								pending: true
+							};
+							pendingLinks[makeLinkKey(link1)] = link1;
+							var link2 = {
+								'src-switch': dstData.dpid,
+								'src-port': 1,
+								'dst-switch': srcData.dpid,
+								'dst-port': 1,
+								pending: true
+							};
+							pendingLinks[makeLinkKey(link2)] = link2;
+							updateTopology();
+
+							linkUp(link1);
+
+							// remove the pending links after 10s
+							setTimeout(function () {
+								delete pendingLinks[makeLinkKey(link1)];
+								delete pendingLinks[makeLinkKey(link2)];
+
+								updateTopology();
+							}, pendingTimeout);
+						}
+					}
+				}
+			}
+
+			clearHighlight();
+		} else {
+			clearHighlight();
+		}
+
+	});
+
+	function labelRingEnter(data) {
+		if (!data.length) {
+			return;
+		}
+
+		// create the nodes
+		var nodes = d3.select(this).selectAll("g")
+			.data(data, function (data) {
+				return data.dpid;
+			})
+			.enter().append("svg:g")
+			.classed('nolabel', true)
+			.attr("id", function (data) {
+				return data.dpid + '-label';
+			})
+			.attr("transform", function(data, i) {
+				return "rotate(" + data.angle+ ")translate(" + data.radius * 150 + ")rotate(" + (-data.angle) + ")";
+			})
+
+		// add the text nodes which show on mouse over
+		nodes.append("svg:text")
+				.text(function (data) {return data.dpid;})
+				.attr("x", function (data) {
+					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
+						if (data.className == 'edge') {
+							return - data.width*3 - 4;
+						} else {
+							return - data.width - 4;
+						}
+					} else {
+						if (data.className == 'edge') {
+							return data.width*3 + 4;
+						} else {
+							return data.width + 4;
+						}
+					}
+				})
+				.attr("y", function (data) {
+					var y;
+					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
+						if (data.className == 'edge') {
+							y = data.width*3/2 + 4;
+						} else {
+							y = data.width/2 + 4;
+						}
+					} else {
+						if (data.className == 'edge') {
+							y = data.width*3/2 + 4;
+						} else {
+							y = data.width/2 + 4;
+						}
+					}
+					return y - 6;
+				})
+				.attr("text-anchor", function (data) {
+					if (data.angle <= 90 || data.angle >= 270 && data.angle <= 360) {
+						return "end";
+					} else {
+						return "start";
+					}
+				})
+				.attr("transform", function(data) {
+					var m = document.querySelector('#viewbox').getTransformToElement().inverse();
+					if (data.scale) {
+						m = m.scale(data.scale);
+					}
+					return "matrix( " + m.a + " " + m.b + " " + m.c + " " + m.d + " " + m.e + " " + m.f + " )";
+				})
+	}
+
+	labelRings.enter().append("svg:g")
+		.attr("class", "textRing")
+		.each(labelRingEnter);
+
+	// switches should not change during operation of the ui so no
+	// rings.exit()
+
+
+	// DRAW THE LINKS
+
+	// key on link dpids since these will come/go during demo
+	var links = d3.select('svg').selectAll('.link').data(links, function (d) {
+			return d['src-switch']+'->'+d['dst-switch'];
+	});
+
+	// add new links
+	links.enter().append("svg:path")
+	.attr("class", "link");
+
+	links.attr('id', function (d) {
+			return makeLinkKey(d);
+		})
+		.attr("d", function (d) {
+			var src = d3.select(document.getElementById(d['src-switch']));
+			var dst = d3.select(document.getElementById(d['dst-switch']));
+
+			var srcPt = document.querySelector('svg').createSVGPoint();
+			srcPt.x = src.attr('x');
+			srcPt.y = src.attr('y');
+			srcPt = srcPt.matrixTransform(src[0][0].getCTM());
+
+			var dstPt = document.querySelector('svg').createSVGPoint();
+			dstPt.x = dst.attr('x');
+			dstPt.y = dst.attr('y');
+			dstPt = dstPt.matrixTransform(dst[0][0].getCTM());
+
+			var midPt = document.querySelector('svg').createSVGPoint();
+			midPt.x = (srcPt.x + dstPt.x)/2;
+			midPt.y = (srcPt.y + dstPt.y)/2;
+
+			return line([srcPt, midPt, dstPt]);
+		})
+		.attr("marker-mid", function(d) { return "url(#arrow)"; })
+		.classed('pending', function (d) {
+			return d.pending;
+		});
+
+
+	// remove old links
+	links.exit().remove();
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/utils.js b/web/ons-demo/js/utils.js
index 4f6d0c1..d597507 100644
--- a/web/ons-demo/js/utils.js
+++ b/web/ons-demo/js/utils.js
@@ -1,3 +1,6 @@
+/***************************************************************************************************
+extract url parameters into a map
+***************************************************************************************************/
 function parseURLParameters() {
 	var parameters = {};
 
@@ -13,12 +16,72 @@
 	return parameters;
 }
 
-function findLink(model, dpid) {
-	var links = [];
-	model.links.forEach(function (link) {
-		if (link['src-switch'] == dpid || link['dst-switch'] == dpid) {
-			links.push(link);
-		}
-	});
-	return links;
-}
\ No newline at end of file
+/***************************************************************************************************
+convenience function for moving an SVG element to the front so that it draws on top
+***************************************************************************************************/
+d3.selection.prototype.moveToFront = function() {
+  return this.each(function(){
+    this.parentNode.appendChild(this);
+  });
+};
+
+/***************************************************************************************************
+standard function for generating the 'd' attribute for a path from an array of points
+***************************************************************************************************/
+var line = d3.svg.line()
+    .x(function(d) {
+    	return d.x;
+    })
+    .y(function(d) {
+    	return d.y;
+    });
+
+
+/***************************************************************************************************
+starts the "pending" animation
+***************************************************************************************************/
+function setPending(selection) {
+	selection.classed('pending', false);
+	setTimeout(function () {
+		selection.classed('pending', true);
+	}, 0);
+}
+
+/***************************************************************************************************
+convert angle in degrees to radians
+***************************************************************************************************/
+function toRadians (degrees) {
+  return degrees * (Math.PI / 180);
+}
+
+/***************************************************************************************************
+used to generate DOM element id for this link
+***************************************************************************************************/
+function makeLinkKey(link) {
+	return link['src-switch'] + '=>' + link['dst-switch'];
+}
+
+/***************************************************************************************************
+used to generate DOM element id for this flow in the topology view
+***************************************************************************************************/
+function makeFlowKey(flow) {
+	return flow.srcDpid + '=>' + flow.dstDpid;
+}
+
+/***************************************************************************************************
+used to generate DOM element id for this flow in the selected flows table
+***************************************************************************************************/
+function makeSelectedFlowKey(flow) {
+	return 'S' + makeFlowKey(flow);
+}
+
+/***************************************************************************************************
+update the app header using the current model
+***************************************************************************************************/
+function updateHeader() {
+	d3.select('#lastUpdate').text(new Date());
+	d3.select('#activeSwitches').text(model.edgeSwitches.length + model.aggregationSwitches.length + model.coreSwitches.length);
+	d3.select('#activeFlows').text(model.flows.length);
+}
+
+