Merge remote-tracking branch 'upstream/master'
diff --git a/web/ons-demo/assets/eye.svg b/web/ons-demo/assets/eye.svg
new file mode 100644
index 0000000..73596c2
--- /dev/null
+++ b/web/ons-demo/assets/eye.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
+<g>
+	<path d="M506.637,242.501c-5.362-6.347-11.263-12.33-17.171-18.193c-31.897-31.679-68.549-59.921-108.648-80.411
+		c-25.618-13.08-53.038-23.655-81.451-28.721c-14.453-2.586-28.617-3.912-43.474-3.938c-14.447,0.025-28.908,1.353-43.361,3.938
+		c-28.412,5.065-55.775,15.641-81.393,28.721c-40.102,20.489-76.724,48.733-108.622,80.411
+		c-5.909,5.862-11.794,11.847-17.155,18.193c-7.147,8.484-7.147,18.515,0,27c16.344,19.353,35.774,36.575,55.542,52.321
+		c42.57,33.915,91.25,62.278,144.993,73.711c16.621,3.524,33.299,5.244,49.998,5.228c16.904,0.018,33.488-1.702,50.107-5.228
+		c53.744-11.433,102.534-39.796,145.104-73.711c19.768-15.745,39.194-32.969,55.538-52.321
+		C513.79,261.018,513.783,250.987,506.637,242.501z M255.892,354.552c-54.334-0.104-98.348-44.177-98.348-98.554
+		c0-54.351,44.014-98.438,98.348-98.543c54.809,0.104,98.347,44.192,98.347,98.543C354.24,310.374,310.701,354.445,255.892,354.552z
+		"/>
+	<path d="M255.86,217.881c-21.06,0-38.106,17.059-38.106,38.115c0,21.068,17.047,38.123,38.106,38.123
+		c21.058,0,38.124-17.055,38.124-38.123C293.984,234.94,276.917,217.881,255.86,217.881z"/>
+</g>
+</svg>
diff --git a/web/ons-demo/css/layout.default.css b/web/ons-demo/css/layout.default.css
index fae757d..e46f165 100644
--- a/web/ons-demo/css/layout.default.css
+++ b/web/ons-demo/css/layout.default.css
@@ -4,6 +4,13 @@
 
 body {
 	display: -webkit-box;
+	-webkit-box-orient: vertical;
+	-webkit-user-select: none;
+}
+
+#columns {
+	display: -webkit-box;
+	-webkit-box-flex: 1.0;
 }
 
 .header {
@@ -39,6 +46,7 @@
 
 .selectedFlow {
 	display: -webkit-box;
+	-webkit-user-select: auto;
 }
 
 #selectedFlowsHeader {
@@ -62,24 +70,11 @@
 	-webkit-box-flex: 1.0;
 }
 
-.selectedFlow {
-	border-bottom: 1px solid white;
-}
-
-.selectedFlow:last-child {
-	border-bottom: none;
-}
-
-
 #controllers {
 	display: -webkit-box;
 	-webkit-box-orient: vertical;
 }
 
-#controllers .header {
-	-webkit-box-pack: center;
-	border-bottom: 1px solid white;
-}
 
 #controllerList {
 	display: -webkit-box;
diff --git a/web/ons-demo/css/skin.default.css b/web/ons-demo/css/skin.default.css
index adc4e18..579d940 100644
--- a/web/ons-demo/css/skin.default.css
+++ b/web/ons-demo/css/skin.default.css
@@ -25,13 +25,6 @@
 	padding: .25em;
 }
 
-.status.bottom {
-	position: absolute;
-	bottom: 0px;
-	right: 0px;
-	font-size: 12px;
-}
-
 #status.top span {
 	font-size: 24px;
 }
@@ -40,7 +33,7 @@
 	padding: 1em;
 	background-color: lightgray;
 	color: black;
-	border: 1px solid white;
+	border: 1px solid #AAA;
 }
 
 #arrow {
@@ -52,6 +45,27 @@
 	height: 50px;
 }
 
+#topology {
+	border-top: 1px solid #AAA;
+}
+
+.selectedFlow {
+	border-bottom: 1px solid #AAA;
+}
+
+.selectedFlow:last-child {
+	border-bottom: none;
+}
+
+#lastUpdated {
+	padding-bottom: 0px;
+}
+
+#controllers .header {
+	-webkit-box-pack: center;
+	border-bottom: 1px solid #AAA;
+}
+
 
 #right .header {
 	font-size: 12px;
@@ -60,11 +74,11 @@
 }
 
 #controllers, #selectedFlows {
-	border-top: 1px solid white;
+	border-top: 1px solid #AAA;
 }
 
 #selectedFlows {
-	border-bottom: 1px solid white;
+	border-bottom: 1px solid #AAA;
 }
 
 .selectedFlow {
@@ -78,14 +92,23 @@
 	background-color:#AAA;
 }
 
+circle.highlight {
+	stroke: rgba(255, 255, 255, .5);
+	stroke-width: 2px;
+}
+
+path {
+	pointer-events: none;
+}
+
 path.flow {
 	fill: none;
-	stroke-width: 2px;
+	stroke-width: 3px;
 	stroke: rgba(255, 255, 255, .35);
 }
 
 #selectedFlowsHeader {
-	border-top: 1px solid white;
+	border-top: 1px solid #AAA;
 }
 
 .flowIndex, .flowId, .srcDPID, .dstDPID, .iperf {
@@ -95,16 +118,30 @@
 }
 
 .flowIndex, .flowId, .srcDPID, .dstDPID {
-	border-right: 1px solid white;
+	border-right: 1px solid #AAA;
 }
 
 
 #controllers {
-	border-right: 1px solid white;
+	border-right: 1px solid #AAA;
 }
 
 .controller {
 	padding: .25em;
+	padding-left: 2.5em;
+	position: relative;
+}
+
+.controllerEye {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	height: 100%;
+	width: 2em;
+	background-image: url('../assets/eye.svg');
+	background-size: auto 100%;
+	background-repeat: no-repeat;
+	background-position: .25em center;
 }
 
 #logo {
@@ -245,3 +282,22 @@
 	fill: #6949D7;
 	background-color: #6949D7;
 }
+
+
+@-webkit-keyframes pending {
+	from {
+		opacity: 1.0;
+	}
+	to {
+		opacity: 0.5;
+	}
+}
+
+.pending {
+	-webkit-animation-name: pending;
+	-webkit-animation-duration: .5s;
+	-webkit-animation-direction: alternate;
+	-webkit-animation-timing-function: ease-in-out;
+	-webkit-animation-iteration-count: 24;
+}
+
diff --git a/web/ons-demo/index.html b/web/ons-demo/index.html
index 21727db..e89f409 100644
--- a/web/ons-demo/index.html
+++ b/web/ons-demo/index.html
@@ -7,42 +7,44 @@
 	<script src="js/async.js"></script>
 	<script src="js/utils.js"></script>
 	<script src="js/model.js"></script>
+	<script src="js/controller.js"></script>
 </head>
 
 <body>
 
-<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='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='controllers'>
+			<div class='header'>ONOS Nodes</div>
+			<div id='controllerList'></div>
+		</div>
 	</div>
-	<div id='selectedFlowsHeader'>
-		<div class='flowIndex'></div>
-		<div class='flowId'>flow id</div>
-		<div class='srcDPID'>src</div>
-		<div class='dstDPID'>dst</div>
-		<div class='iperf'>iperf</div>
-	</div>
-	<div id='selectedFlows'></div>
-	<div id='topology'>
-		<div id='svg-container'></div>
-		<div class='status bottom'><span class='static'>Last updated:</span><span id='lastUpdate' class='dynamic'>Mon Mar 18 11:11:12 PDT 2013</span></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 id='selectedFlowsHeader'>
+	<div class='flowIndex'></div>
+	<div class='flowId'>flow id</div>
+	<div class='srcDPID'>src</div>
+	<div class='dstDPID'>dst</div>
+	<div class='iperf'>iperf</div>
+</div>
+<div id='selectedFlows'></div>
 
 <script src="js/app.js"></script>
 </body>
diff --git a/web/ons-demo/js/app.js b/web/ons-demo/js/app.js
index ab5cfda..daf767c 100644
--- a/web/ons-demo/js/app.js
+++ b/web/ons-demo/js/app.js
@@ -14,7 +14,10 @@
     	return d.y;
     });
 
+var model;
 var svg, selectedFlowsView;
+var updateTopology;
+var pendingLinks = {};
 
 var colors = [
 	'color1',
@@ -34,9 +37,21 @@
 
 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(svg, model);
+	});
+
 	var svg = d3.select('#svg-container').append('svg:svg');
 
 	svg.append("svg:defs").append("svg:marker")
@@ -56,8 +71,6 @@
 var selectedFlowsData = [
 	{selected: false, flow: null},
 	{selected: false, flow: null},
-	{selected: false, flow: null},
-	{selected: false, flow: null},
 	{selected: false, flow: null}
 ];
 
@@ -99,6 +112,8 @@
 		}
 	})
 
+	// "marching ants"
+	// TODO: this will only be true if there's an iperf session running
 	flows.select('animate').attr('from', function (d) {
 		if (d.flow) {
 			if (d.selected) {
@@ -192,22 +207,28 @@
   return angle * (Math.PI / 180);
 }
 
+var widths = {
+	edge: 6,
+	aggregation: 12,
+	core: 18
+}
+
 function createRingsFromModel(model) {
 	var rings = [{
 		radius: 3,
-		width: 6,
+		width: widths.edge,
 		switches: model.edgeSwitches,
 		className: 'edge',
 		angles: []
 	}, {
 		radius: 2.25,
-		width: 12,
+		width: widths.aggregation,
 		switches: model.aggregationSwitches,
 		className: 'aggregation',
 		angles: []
 	}, {
 		radius: 0.75,
-		width: 18,
+		width: widths.core,
 		switches: model.coreSwitches,
 		className: 'core',
 		angles: []
@@ -289,11 +310,124 @@
 	return testRings;
 }
 
-function updateTopology(svg, model) {
+function makeLinkKey(link) {
+	return link['src-switch'] + '=>' + link['dst-switch'];
+}
+
+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;
+}
+
+updateTopology = function(svg, model) {
 
 	// DRAW THE SWITCHES
 	var rings = svg.selectAll('.ring').data(createRingsFromModel(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]);
+	}
+
+	var linkMap = createLinkMap(links);
+//	var flowMap = createFlowMap(model);
+
+	function mouseOverSwitch(data) {
+		if (data.highlighted) {
+			return;
+		}
+
+		// only highlight valid link or flow destination by checking for class of existing highlighted circle
+		var highlighted = svg.selectAll('circle.highlight')[0];
+		if (highlighted.length == 1) {
+			var s = d3.select(highlighted[0]);
+			// 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;
+		}
+
+
+		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
+		var node = d3.select(document.getElementById(data.dpid));
+		node.select('circle').classed('highlight', true).transition().duration(100).attr("r", widths.core);
+		data.highlighted = true;
+		node.moveToFront();
+	}
+
+	function mouseOutSwitch(data) {
+		if (data.mouseDown)
+			return;
+
+		d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
+		var node = d3.select(document.getElementById(data.dpid));
+		node.select('circle').classed('highlight', false).transition().duration(100).attr("r", widths[data.className]);
+		data.highlighted = false;
+		data.target = false;
+	}
+
+	function mouseDownSwitch(data) {
+		mouseOverSwitch(data);
+		data.mouseDown = true;
+	}
+
+	function mouseUpSwitch(data) {
+		if (data.mouseDown) {
+			data.mouseDown = false;
+			d3.event.stopPropagation();
+		}
+	}
+
+	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;
@@ -305,7 +439,6 @@
 				return data.dpid;
 			})
 			.enter().append("svg:g")
-			.classed('nolabel', true)
 			.attr("id", function (data, i) {
 				return data.dpid;
 			})
@@ -333,16 +466,15 @@
 			});
 
 		// setup the mouseover behaviors
-		function showLabel(data, index) {
-			d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', false);
-		}
+		nodes.on('mouseover', mouseOverSwitch);
+		nodes.on('mouseout', mouseOutSwitch);
+		nodes.on('mouseup', mouseUpSwitch);
+		nodes.on('mousedown', mouseDownSwitch);
 
-		function hideLabel(data, index) {
-			d3.select(document.getElementById(data.dpid + '-label')).classed('nolabel', true);
+		// only do switch up/down for core switches
+		if (i == 2) {
+			nodes.on('dblclick', doubleClickSwitch);
 		}
-
-		nodes.on('mouseover', showLabel);
-		nodes.on('mouseout', hideLabel);
 	}
 
 	// append switches
@@ -356,11 +488,20 @@
 			.data(data, function (data) {
 				return data.dpid;
 			});
-		nodes.select('circle').attr('class', function (data, i)  {
-				if (data.state === 'ACTIVE') {
-					return data.className + ' ' + controllerColorMap[data.controller];
+		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 + ' ' + 'colorInactive';
+					return data.className + ' inactive ' + 'colorInactive';
 				}
 			});
 	}
@@ -374,6 +515,106 @@
 	// always on top
 	var labelRings = svg.selectAll('.labelRing').data(createRingsFromModel(model));
 
+	d3.select(document.body).on('mouseup', function () {
+		function clearHighlight() {
+			svg.selectAll('circle').each(function (data) {
+				data.mouseDown = false;
+				mouseOutSwitch(data);
+			})
+		};
+
+		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('circle.highlight')[0];
+		if (highlighted.length == 2) {
+			var s1Data = d3.select(highlighted[0]).data()[0];
+			var s2Data = d3.select(highlighted[1]).data()[0];
+
+			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)) {
+					alert('do create flow');
+				} else {
+					alert('do not create flow');
+				}
+			} 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(svg, model);
+
+							linkUp(link1);
+
+							// remove the pending link after 10s
+							setTimeout(function () {
+								delete pendingLinks[makeLinkKey(link1)];
+								delete pendingLinks[makeLinkKey(link2)];
+
+								updateTopology(svg, model);
+							}, 10000);
+						}
+					}
+				}
+			}
+
+			clearHighlight();
+		} else {
+			clearHighlight();
+		}
+
+	});
+
 	function labelRingEnter(data) {
 		if (!data.length) {
 			return;
@@ -391,7 +632,7 @@
 			})
 			.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")
@@ -448,61 +689,49 @@
 		.attr("class", "textRing")
 		.each(labelRingEnter);
 
-
 	// switches should not change during operation of the ui so no
 	// rings.exit()
 
 
-	// do mouseover zoom on edge nodes
-	function zoom(data, index) {
-		var g = d3.select(document.getElementById(data.dpid)).select('circle');
-			g.transition().duration(100).attr("r", data.width*3);
-			// TODO: this doesn't work because the data binding is by index
-			d3.select(this.parentNode).moveToFront();
-	}
-
-	svg.selectAll('.edge').on('mouseover', zoom);
-	svg.selectAll('.edge').on('mousedown', zoom);
-
-	function unzoom(data, index) {
-		var g = d3.select(document.getElementById(data.dpid)).select('circle');
-			g.transition().duration(100).attr("r", data.width);
-	}
-	svg.selectAll('.edge').on('mouseout', unzoom);
-
-
 	// DRAW THE LINKS
 
 	// key on link dpids since these will come/go during demo
-	var links = d3.select('svg').selectAll('.link').data(model.links, function (d) {
+	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")
-	.attr("d", function (d) {
+	.attr("class", "link");
 
-		var src = d3.select(document.getElementById(d['src-switch']));
-		var dst = d3.select(document.getElementById(d['dst-switch']));
+	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 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'); // tmp: make up and down links distinguishable
-		dstPt = dstPt.matrixTransform(dst[0][0].getCTM());
+			var dstPt = document.querySelector('svg').createSVGPoint();
+			dstPt.x = dst.attr('x');
+			dstPt.y = dst.attr('y'); // tmp: make up and down links distinguishable
+			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;
+			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)"; });
+			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();
@@ -520,7 +749,9 @@
 		})
 		.text(function (d) {
 			return d;
-		});
+		})
+		.append('div')
+		.attr('class', 'controllerEye');
 
 	controllers.attr('class', function (d) {
 			var color = 'colorInactive';
@@ -534,7 +765,23 @@
 	// this should never be needed
 	// controllers.exit().remove();
 
-	controllers.on('click', function (c, index) {
+	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('.controllerEye').on('click', function (c) {
 		var allSelected = true;
 		for (var key in controllerColorMap) {
 			if (!d3.select(document.body).classed(controllerColorMap[key] + '-selected')) {
@@ -555,20 +802,21 @@
 		// var selected = d3.select(document.body).classed(controllerColorMap[c] + '-selected');
 		// d3.select(document.body).classed(controllerColorMap[c] + '-selected', !selected);
 	});
+
+
 }
 
-var oldModel;
 function sync(svg, selectedFlowsView) {
 	var d = Date.now();
 	updateModel(function (newModel) {
-		console.log('Update time: ' + (Date.now() - d)/1000 + 's');
+//		console.log('Update time: ' + (Date.now() - d)/1000 + 's');
 
-		if (!oldModel || JSON.stringify(oldModel) != JSON.stringify(newModel)) {
+		if (!model || JSON.stringify(model) != JSON.stringify(newModel)) {
 			updateControllers(newModel);
 
 	// fake flows right now
 	var i;
-	for (i = 0; i < newModel.flows.length; i+=1) {
+	for (i = 0; i < newModel.flows.length && i < selectedFlowsData.length; i+=1) {
 		var selected = selectedFlowsData[i] ? selectedFlowsData[i].selected : false;
 		selectedFlowsData[i].flow = newModel.flows[i];
 		selectedFlowsData[i].selected = selected;
@@ -577,11 +825,11 @@
 			updateFlowView(newModel);
 			updateTopology(svg, newModel);
 		} else {
-			console.log('no change');
+//			console.log('no change');
 		}
 		updateHeader(newModel);
 
-		oldModel = newModel;
+		model = newModel;
 
 		// do it again in 1s
 		setTimeout(function () {
diff --git a/web/ons-demo/js/controller.js b/web/ons-demo/js/controller.js
new file mode 100644
index 0000000..a36ae1c
--- /dev/null
+++ b/web/ons-demo/js/controller.js
@@ -0,0 +1,66 @@
+/*global d3*/
+
+function callURL(url) {
+	d3.text(url, function (error, result) {
+		if (error) {
+			alert(url + ' : ' + error.status);
+		} else {
+			console.log(result);
+		}
+	});
+}
+
+
+var controllerFunctions = {
+	l: function (cmd, link) {
+		var url = '/proxy/gui/link/' + [cmd, link['src-switch'], link['src-port'], link['dst-switch'], link['dst-port']].join('/');
+		callURL(url);
+
+	},
+	s: function (cmd, s) {
+		var url = '/proxy/gui/switch/' + [cmd, s.dpid].join('/');
+		callURL(url);
+	},
+	c: function (cmd, c) {
+		var url = '/proxy/gui/controller/' + [cmd, c].join('/');
+		callURL(url);
+	}
+};
+
+
+// if (parseURLParameters().mock) {
+// 	urls = mockURLs;
+// }
+
+
+function linkUp(link) {
+	controllerFunctions.l('up', link);
+}
+
+function linkDown(link) {
+	controllerFunctions.l('down', link);
+}
+
+function switchUp(s) {
+	controllerFunctions.s('up', s);
+}
+
+function switchDown(s) {
+	controllerFunctions.s('down', s);
+}
+
+function controllerUp(c) {
+	controllerFunctions.c('up', c);
+}
+
+function controllerDown(c) {
+	controllerFunctions.c('down', c);
+}
+
+function createFlow(src, dst) {
+
+}
+
+function deleteFlow(src, dst) {
+
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/model.js b/web/ons-demo/js/model.js
index 5b66421..29859a6 100644
--- a/web/ons-demo/js/model.js
+++ b/web/ons-demo/js/model.js
@@ -56,7 +56,7 @@
 var urls = {
 	links: '/wm/core/topology/links/json',
 	switches: '/wm/core/topology/switches/all/json',
-	flows: '/wm/flow/getall/json',
+	flows: '/wm/flow/getsummary/0/0/json?proxy',
 	activeControllers: '/wm/registry/controllers/json',
 	controllers: 'data/controllers.json',
 	mapping: '/wm/registry/switches/json',
@@ -76,7 +76,7 @@
 var proxyURLs = {
 	links: '/wm/core/topology/links/json?proxy',
 	switches: '/wm/core/topology/switches/all/json?proxy',
-	flows: '/wm/flow/getall/json?proxy',
+	flows: '/wm/flow/getsummary/0/0/json?proxy',
 	activeControllers: '/wm/registry/controllers/json?proxy',
 	controllers: 'data/controllers.json',
 	mapping: '/wm/registry/switches/json?proxy',
diff --git a/web/topology_rest.py b/web/topology_rest.py
index f038046..8a94ef9 100755
--- a/web/topology_rest.py
+++ b/web/topology_rest.py
@@ -69,13 +69,55 @@
     response.headers["Content-type"] = "text/css"
   elif suffix == "png":
     response.headers["Content-type"] = "image/png"
+  elif suffix == "svg":
+    response.headers["Content-type"] = "image/svg+xml"
 
   return response
 
 ## PROXY API (allows development where the webui is served from someplace other than the controller)##
 ONOS_GUI3_HOST="http://gui3.onlab.us:8080"
+ONOS_GUI3_CONTROL_HOST="http://gui3.onlab.us:8081"
 ONOS_LOCAL_HOST="http://localhost:8080" ;# for Amazon EC2
 
+@app.route("/proxy/gui/link/<cmd>/<src_dpid>/<src_port>/<dst_dpid>/<dst_port>")
+def proxy_link_change(cmd, src_dpid, src_port, dst_dpid, dst_port):
+  try:
+    command = "curl -s %s/gui/link/%s/%s/%s/%s/%s" % (ONOS_GUI3_CONTROL_HOST, cmd, src_dpid, src_port, dst_dpid, dst_port)
+    print command
+    result = os.popen(command).read()
+  except:
+    print "REST IF has issue"
+    exit
+
+  resp = Response(result, status=200, mimetype='application/json')
+  return resp
+
+@app.route("/proxy/gui/switch/<cmd>/<dpid>")
+def proxy_switch_status_change(cmd, dpid):
+  try:
+    command = "curl -s %s/gui/switch/%s/%s" % (ONOS_GUI3_CONTROL_HOST, cmd, dpid)
+    print command
+    result = os.popen(command).read()
+  except:
+    print "REST IF has issue"
+    exit
+
+  resp = Response(result, status=200, mimetype='application/json')
+  return resp
+
+@app.route("/proxy/gui/controller/<cmd>/<controller_name>")
+def proxy_controller_status_change(cmd, controller_name):
+  try:
+    command = "curl -s %s/gui/controller/%s/%s" % (ONOS_GUI3_CONTROL_HOST, cmd, controller_name)
+    print command
+    result = os.popen(command).read()
+  except:
+    print "REST IF has issue"
+    exit
+
+  resp = Response(result, status=200, mimetype='application/json')
+  return resp
+
 @app.route("/wm/core/topology/switches/all/json")
 def switches():
   if request.args.get('proxy') == None:
@@ -85,7 +127,7 @@
 
   try:
     command = "curl -s %s/wm/core/topology/switches/all/json" % (host)
-    print command
+#    print command
     result = os.popen(command).read()
   except:
     print "REST IF has issue"
@@ -121,7 +163,7 @@
 
   try:
     command = "curl -s %s/wm/flow/getsummary/0/0/json" % (host)
-    print command
+#    print command
     result = os.popen(command).read()
   except:
     print "REST IF has issue"
@@ -139,7 +181,7 @@
 
   try:
     command = "curl -s %s/wm/registry/controllers/json" % (host)
-    print command
+#    print command
     result = os.popen(command).read()
   except:
     print "REST IF has issue"
@@ -157,7 +199,7 @@
 
   try:
     command = "curl -s %s/wm/registry/switches/json" % (host)
-    print command
+#    print command
     result = os.popen(command).read()
   except:
     print "REST IF has issue"
@@ -276,7 +318,7 @@
       if i < len(flowEntries) - 1:
         sdpid= flowEntries[i]['dpid']['value']
         ddpid = flowEntries[i+1]['dpid']['value']
-        path.append( (sdpid, ddpid))  
+        path.append( (sdpid, ddpid))
 
   try:
     command = "curl -s \'http://%s:%s/wm/core/topology/links/json\'" % (RestIP, RestPort)
@@ -359,12 +401,12 @@
     parsedResult = []
 #    exit(1)
 
-  path = [];    
+  path = [];
   for i, v in enumerate(parsedResult):
     if i < len(parsedResult) - 1:
       sdpid= parsedResult[i]['switch']
       ddpid = parsedResult[i+1]['switch']
-      path.append( (sdpid, ddpid))  
+      path.append( (sdpid, ddpid))
 
   try:
     command = "curl -s \'http://%s:%s/wm/core/topology/links/json\'" % (RestIP, RestPort)
@@ -488,7 +530,7 @@
         ret = {}
         ret[switchId]=aggr
     else:
-        ret = {} 
+        ret = {}
 
     js = json.dumps(ret)
     resp = Response(js, status=200, mimetype='application/json')
@@ -543,8 +585,8 @@
   resp = Response(js, status=200, mimetype='application/json')
   return resp
 
-topo_less = { 
-  "nodes" : [ 
+topo_less = {
+  "nodes" : [
     {"name" : "00:a0", "group" : 1},
     {"name" : "00:a1", "group" : 1},
     {"name" : "00:a2", "group" : 1},
@@ -559,8 +601,8 @@
     ]
 }
 
-topo_more = { 
-  "nodes" : [ 
+topo_more = {
+  "nodes" : [
     {"name" : "00:a3", "group" : 2},
     {"name" : "00:a0", "group" : 1},
     {"name" : "00:a1", "group" : 1},