fanout logic for layout. almost done.
diff --git a/web/ons-demo/css/skin.default.css b/web/ons-demo/css/skin.default.css
index 179000e..4efa334 100644
--- a/web/ons-demo/css/skin.default.css
+++ b/web/ons-demo/css/skin.default.css
@@ -18,7 +18,7 @@
 
 #map path {
 	fill:none;
-	stroke:#888;
+	stroke:#333;
 	stroke-width:2;
 }
 
diff --git a/web/ons-demo/data/configuration.json b/web/ons-demo/data/configuration.json
index 783a4de..27d2393 100644
--- a/web/ons-demo/data/configuration.json
+++ b/web/ons-demo/data/configuration.json
@@ -40,34 +40,40 @@
 	},
 	"geo": {
 		"00:00:00:08:a2:08:f9:01": {
-			"lat": 39,
-			"lng": -98,
-			"label": "LAX"
+			"lat": 41.891033,
+			"lng": -87.628326,
+			"label": "CHI",
+			"fanOutAngle": 180
 		},
 		"00:00:00:00:ba:5e:ba:11": {
-			"lat": 0,
-			"lng": 0,
-			"label": ""
+			"lat": 47.611024,
+			"lng": -122.33242,
+			"label": "SEA",
+			"fanOutAngle": 270
 		},
 		"00:00:20:4e:7f:51:8a:35": {
-			"lat": 0,
-			"lng": 0,
-			"label": ""
+			"lat": 33.758599,
+			"lng": -84.387360,
+			"label": "ATL",
+			"fanOutAngle": 0
 		},
 		"00:00:00:00:00:00:ba:12": {
-			"lat": 0,
-			"lng": 0,
-			"label": ""
+			"lat": 41.225925,
+			"lng": -74.00528,
+			"label": "NYC",
+			"fanOutAngle": 135
 		},
 		"00:00:00:00:ba:5e:ba:13": {
-			"lat": 0,
-			"lng": 0,
-			"label": ""
+			"lat": 37.901187,
+			"lng": -76.037163,
+			"label": "DC",
+			"fanOutAngle": 45
 		},
 		"00:00:00:16:97:08:9a:46": {
-			"lat": 0,
-			"lng": 0,
-			"label": ""
+			"lat": 34.102708,
+			"lng": -118.238983,
+			"label": "LA",
+			"fanOutAngle": 300
 		}
 	}
 }
\ No newline at end of file
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
diff --git a/web/ons-demo/js/rings.js b/web/ons-demo/js/rings.js
index ab7be1d..29907f6 100644
--- a/web/ons-demo/js/rings.js
+++ b/web/ons-demo/js/rings.js
@@ -16,6 +16,52 @@
 	cb();
 }
 
+function updateLinkLines() {
+
+	// key on link dpids since these will come/go during demo
+	var linkLines = d3.select('svg').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');
+			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
+	linkLines.exit().remove();
+}
+
+
 function createRingTopologyModel(model) {
 	var rings = [{
 		radius: 3,
@@ -269,6 +315,8 @@
 
 	// switches should not change during operation of the ui so no
 	// rings.exit()
+
+	updateLinkLines();
 }
 
 })();
diff --git a/web/ons-demo/js/topology.js b/web/ons-demo/js/topology.js
index 1602abb..b932a1f 100644
--- a/web/ons-demo/js/topology.js
+++ b/web/ons-demo/js/topology.js
@@ -6,59 +6,11 @@
 
 (function () {
 
-function updateLinkLines() {
-
-	// key on link dpids since these will come/go during demo
-	var linkLines = d3.select('svg').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']));
-
-			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
-	linkLines.exit().remove();
-}
-
 updateTopology = function() {
 
 	/* currently rings.js and map.js can be included to define the topology display */
-
 	drawTopology();
 
-
-
-	/* the remainder should work regardless of the topology display */
-
-//	updateLinkLines();
-
 	// setup the mouseover behaviors
 	var allSwitches = d3.selectAll('.edge, .core, .aggregation');