fanout logic for layout. almost done.
diff --git a/web/ons-demo/js/map.js b/web/ons-demo/js/map.js
index 7915187..60c9561 100644
--- a/web/ons-demo/js/map.js
+++ b/web/ons-demo/js/map.js
@@ -4,8 +4,8 @@
 (function () {
 
 var projection = d3.geo.mercator()
-    .center([82, 45])
-    .scale(10000)
+    .center([82, 46])
+    .scale(8000)
     .rotate([-180,0]);
 
 function createMap(svg, cb) {
@@ -27,75 +27,291 @@
 	});
 }
 
+/***************************************************************************************************
 
-var projection
+***************************************************************************************************/
+var switchMap;
+function makeSwitchMap() {
+	switchMap = {};
+	model.coreSwitches.forEach(function (s) {
+		switchMap[s.dpid] = s;
+	});
+	model.aggregationSwitches.forEach(function (s) {
+		switchMap[s.dpid] = s;
+	});
+	model.edgeSwitches.forEach(function (s) {
+		switchMap[s.dpid] = s;
+	});
+}
+
+/***************************************************************************************************
+create a map from edge->aggregation and aggreation->core switches
+***************************************************************************************************/
+var switchAssociations;
+function makeAssociations() {
+	switchAssociations = {};
+
+	var key;
+	for (key in model.configuration.association) {
+		var aggregationSwitches = model.configuration.association[key];
+		aggregationSwitches.forEach(function (s) {
+			switchAssociations[s] = key;
+		});
+	}
+}
+
+/***************************************************************************************************
+get the upstream switch. this only makes sense for aggregation and edge switches
+***************************************************************************************************/
+function getUpstream(dpid, className) {
+	if (className === 'aggregation') {
+		return switchAssociations[dpid];
+	} else if (className === 'edge') {
+		var aggregationDpid = dpid.split(':');
+		aggregationDpid[7] = '01'; // the last component of the agg switch is always '01'
+		return aggregationDpid.join(':');
+	}
+}
+
+
+
+/*****************a**********************************************************************************
+create a map to hold the fanout information for the switches
+***************************************************************************************************/
+var fanouts;
+function makeFanouts() {
+	fanouts = {};
+	model.coreSwitches.forEach(function (s) {
+		fanouts[s.dpid] = model.configuration.geo[s.dpid];
+		fanouts[s.dpid].count = 0;
+	});
+
+	model.aggregationSwitches.forEach(function (s) {
+		fanouts[s.dpid] = {count: 0};
+		var upstreamFanout = fanouts[getUpstream(s.dpid, 'aggregation')];
+		upstreamFanout.count += 1;
+	});
+
+	model.edgeSwitches.forEach(function (s) {
+		fanouts[s.dpid] = {};
+		var upstreamFanout = fanouts[getUpstream(s.dpid, 'edge')];
+		upstreamFanout.count += 1;
+	});
+}
+
+
+var projection;
+var switchLayer;
+var labelsLayer;
+var linksLayer;
 createTopologyView = function (cb) {
 	var svg = createRootSVG();
 
-	createMap(svg, cb);
+	createMap(svg, function () {
+		switchLayer = topology.append('g');
+		labelsLayer = topology.append('g');
+		linksLayer = topology.append('g');
+
+		cb();
+	});
 }
 
-function makeSwitchesModel(switches) {
+function updateLinkLines() {
+
+	// key on link dpids since these will come/go during demo
+	var linkLines = linksLayer.selectAll('.link').data(links, function (d) {
+		return d['src-switch']+'->'+d['dst-switch'];
+	});
+
+	// add new links
+	linkLines.enter().append("svg:path").attr("class", "link");
+
+	linkLines.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']));
+
+			if (src.empty() || dst.empty()) {
+				return "M0,0";
+			}
+
+			var srcPt = document.querySelector('svg').createSVGPoint();
+			srcPt.x = src.attr('x');
+			srcPt.y = src.attr('y');
+
+			var dstPt = document.querySelector('svg').createSVGPoint();
+			dstPt.x = dst.attr('x');
+			dstPt.y = dst.attr('y');
+
+			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
+	linkLines.exit().remove();
+}
+
+var fanOutAngles = {
+	aggregation: 90,
+	edge: 7
+}
+
+function makeSwitchesModel(switches, className) {
 	var switchesModel = [];
 	switches.forEach(function (s) {
+		var geo = model.configuration.geo[s.dpid];
+
+		var pos, label;
+		if (geo) {
+			pos = projection([geo.lng, geo.lat]);
+			label = geo.label;
+		} else {
+			var upstream = getUpstream(s.dpid, className);
+			if (upstream) {
+				var upstreamGeo = fanouts[upstream];
+				pos = projection([upstreamGeo.lng, upstreamGeo.lat]);
+
+				var fanOutAngle = upstreamGeo.fanOutAngle;
+				fanOutAngle -= (upstreamGeo.count - 1) * fanOutAngles[className]/2;
+
+				var angle = toRadians(fanOutAngle);
+				var xOff = Math.sin(angle) * widths[className] * 20;
+				var yOff = Math.cos(angle) * widths[className] * 20;
+
+				pos = [pos[0] + xOff, pos[1] + yOff];
+
+				var fakeGeo = projection.invert(pos);
+
+				var fanout = fanouts[s.dpid];
+				fanout.fanOutAngle = fanOutAngle;
+				fanout.lng = fakeGeo[0];
+				fanout.lat = fakeGeo[1];
+
+				upstreamGeo.fanOutAngle += fanOutAngles[className];
+
+			} else {
+				pos = projection([-98, 39]);
+			}
+		}
+
 		switchesModel.push({
 			dpid: s.dpid,
 			state: s.state,
-			className: 'core',
+			className: className,
 			controller: s.controller,
-			geo: model.configuration.geo[s.dpid]
+			label: label,
+			x: pos[0],
+			y: pos[1]
 		});
 	});
 
 	return switchesModel;
 }
 
-drawTopology = function () {
+function switchEnter(d) {
+	var g = d3.select(this);
+	var width = widths[d.className];
 
+	g.append('svg:circle').attr('r', width)
+		.classed(d.className, true)
+		.attr('cx', d.x)
+		.attr('cy', d.y);
 
-	// enter
-	function switchEnter(s) {
-		var g = d3.select(this);
-
-		g.append('svg:circle').attr('r', widths.core);
-		g.append('svg:text').text(s.geo.label).attr('transform', 'translate(' + widths.core + ' ' + widths.core + ')');
-
+	if (d.label) {
+		g.append('svg:text')
+			.text(d.label)
+			.attr('x', d.x + width)
+			.attr('y', d.y + width);
 	}
+}
 
-	var coreSwitches = topology.selectAll('.core').data(makeSwitchesModel(model.coreSwitches))
+function switchesEnter(switches) {
+	return switchLayer.selectAll('g').data(switches, function (d) {
+		return d.dpid;
+	})
 		.enter()
-		.append('svg:g')
-		.attr("id", function (d) {
-			return d.dpid;
-		})
-		.classed('core', true)
-		.attr("transform", function(d) {
-			if (d.geo) {
-				return "translate(" + projection([d.geo.lng, d.geo.lat]) + ")";
-			}
-		})
-		.each(switchEnter);
+			.append('svg:g')
+				.attr("id", function (d) {
+					return d.dpid;
+				})
+				.attr('x', function (d) {
+					return d.x;
+				})
+				.attr('y', function (d) {
+					return d.y;
+				})
+				.each(switchEnter);
+}
+
+// function labelsEnter(className, model) {
+// 			.enter().append("svg:g")
+// 			.classed('nolabel', true)
+// 			.attr("id", function (data) {
+// 				return data.dpid + '-label';
+// 			})
 
 
+// 	return topology.selectAll('.'+className).data(makeSwitchesModel(model))
+// 		.enter()
+// 			.append('svg:g')
+// 				.attr("id", function (d) {
+// 					return d.dpid;
+// 				})
+// 				.classed('core', true)
+// 				.attr('x', function (d) {
+// 					return d.x;
+// 				})
+// 				.attr('y', function (d) {
+// 					return d.y;
+// 				})
+// 				.each(switchEnter);
+// }
 
-	// update
-	coreSwitches
-		.each(function (data) {
+function switchesUpdate(switches) {
+	switches.each(function (d) {
 			// 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')) {
+			if (d.state === 'ACTIVE' && circle.classed('inactive') ||
+				d.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];
+		.attr('class', function (d)  {
+			if (d.state === 'ACTIVE' && d.controller) {
+				return d.className + ' active ' + controllerColorMap[d.controller];
 			} else {
-				return data.className + ' inactive ' + 'colorInactive';
+				return d.className + ' inactive ' + 'colorInactive';
 			}
 		});
+}
 
+drawTopology = function () {
+
+	makeSwitchMap();
+	makeAssociations();
+	makeFanouts();
+
+	var coreSwitches = makeSwitchesModel(model.coreSwitches, 'core');
+	var aggregationSwitches = makeSwitchesModel(model.aggregationSwitches, 'aggregation');
+	var edgeSwitches = makeSwitchesModel(model.edgeSwitches, 'edge');
+
+	var switches = coreSwitches.concat(aggregationSwitches).concat(edgeSwitches);
+
+	switchesUpdate(switchesEnter(switches));
+
+
+
+
+
+	updateLinkLines();
 }
 
 })();
\ No newline at end of file