refactor for new topology view
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