more refactoring of topology code
diff --git a/web/ons-demo/index.html b/web/ons-demo/index.html
index 9798add..005bbed 100644
--- a/web/ons-demo/index.html
+++ b/web/ons-demo/index.html
@@ -1,22 +1,8 @@
 <html>
-
 <head>
 	<link rel="stylesheet" href="css/layout.default.css" type="text/css"/>
 	<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' src='assets/logo.svg'/>
@@ -51,8 +37,21 @@
 
 	<div id='flowChooser'></div>
 
+	<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/rings.js"></script>
+	<script src="js/topologyactions.js"></script>
+	<script src="js/topology.js"></script>
+	<script src="js/flows.js"></script>
+	<script src="js/init.js"></script>
 	<script src="js/app.js"></script>
-
 </div>
 </body>
 
diff --git a/web/ons-demo/js/app.js b/web/ons-demo/js/app.js
index f7eb5b4..34ff2da 100644
--- a/web/ons-demo/js/app.js
+++ b/web/ons-demo/js/app.js
@@ -21,6 +21,8 @@
 			if (modelChanged) {
 				updateControllers();
 				updateSelectedFlows();
+				reconcilePendingLinks(model);
+				updateLinkMap(links);
 				updateTopology();
 			}
 
diff --git a/web/ons-demo/js/globals.js b/web/ons-demo/js/globals.js
index 30f42f2..c7cf5bb 100644
--- a/web/ons-demo/js/globals.js
+++ b/web/ons-demo/js/globals.js
@@ -26,6 +26,16 @@
 var pendingLinks = {};
 
 /***************************************************************************************************
+current links including pending
+***************************************************************************************************/
+var links;
+
+/***************************************************************************************************
+a map from srcDPID => map of dstDPID=>link
+***************************************************************************************************/
+var linkMap;
+
+/***************************************************************************************************
 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
 ***************************************************************************************************/
diff --git a/web/ons-demo/js/map.js b/web/ons-demo/js/map.js
new file mode 100644
index 0000000..7cbaaaf
--- /dev/null
+++ b/web/ons-demo/js/map.js
@@ -0,0 +1,11 @@
+// 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)')
+//   }
+// });
\ No newline at end of file
diff --git a/web/ons-demo/js/rings.js b/web/ons-demo/js/rings.js
new file mode 100644
index 0000000..5141195
--- /dev/null
+++ b/web/ons-demo/js/rings.js
@@ -0,0 +1,126 @@
+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)');
+}
+
+var widths = {
+	edge: 6,
+	aggregation: 12,
+	core: 18
+}
+
+function createTopologyModel(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;
+}
\ No newline at end of file
diff --git a/web/ons-demo/js/topology.js b/web/ons-demo/js/topology.js
index b7a9112..e934972 100644
--- a/web/ons-demo/js/topology.js
+++ b/web/ons-demo/js/topology.js
@@ -4,298 +4,12 @@
 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;
-}
+(function () {
 
 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);
-			}
-		}
-	}
+	var rings = svg.selectAll('.ring').data(createTopologyModel(model));
 
 	function ringEnter(data, i) {
 		if (!data.length) {
@@ -382,7 +96,7 @@
 	// 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));
+	var labelRings = svg.selectAll('.labelRing').data(createTopologyModel(model));
 
 	d3.select(document.body).on('mousemove', function () {
 		if (!d3.select('#topology').classed('linking')) {
@@ -426,132 +140,6 @@
 		}
 	});
 
-	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;
@@ -633,15 +221,15 @@
 	// 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) {
+	var linkLines = d3.select('svg').selectAll('.link').data(links, function (d) {
 			return d['src-switch']+'->'+d['dst-switch'];
 	});
 
 	// add new links
-	links.enter().append("svg:path")
+	linkLines.enter().append("svg:path")
 	.attr("class", "link");
 
-	links.attr('id', function (d) {
+	linkLines.attr('id', function (d) {
 			return makeLinkKey(d);
 		})
 		.attr("d", function (d) {
@@ -671,5 +259,7 @@
 
 
 	// remove old links
-	links.exit().remove();
-}
\ No newline at end of file
+	linkLines.exit().remove();
+}
+
+})();
diff --git a/web/ons-demo/js/topologyactions.js b/web/ons-demo/js/topologyactions.js
new file mode 100644
index 0000000..a7ceaaa
--- /dev/null
+++ b/web/ons-demo/js/topologyactions.js
@@ -0,0 +1,229 @@
+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;
+		}
+
+		// 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);
+		}
+	}
+}
+
+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();
+	}
+});
\ No newline at end of file
diff --git a/web/ons-demo/js/utils.js b/web/ons-demo/js/utils.js
index d597507..e85eeae 100644
--- a/web/ons-demo/js/utils.js
+++ b/web/ons-demo/js/utils.js
@@ -84,4 +84,36 @@
 	d3.select('#activeFlows').text(model.flows.length);
 }
 
+/***************************************************************************************************
+update the global linkmap
+***************************************************************************************************/
+function updateLinkMap(links) {
+	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;
+	});
+}
+
+/***************************************************************************************************
+// removes links from the pending list that are now in the model
+***************************************************************************************************/
+function reconcilePendingLinks(model) {
+	links = [];
+	model.links.forEach(function (link) {
+		links.push(link);
+		delete pendingLinks[makeLinkKey(link)]
+	})
+	var linkId;
+	for (linkId in pendingLinks) {
+		links.push(pendingLinks[linkId]);
+	}
+}
+