Updated fn-spec to include classNames
Removed Classnames file and added code to fn.js
Fixed typo dimentions to dimensions
Moved Device/Link logic from Topo2D3 into the model
Model now calls onChange when any property is changed via the set Method

WIP - Added d3 force layout for devices and lines

Change-Id: I4d1afd3cd4cecf2f719e27f4be5d1e874bd9e342
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 77c2b96..33ad2f6 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -386,6 +386,34 @@
     }
 
 
+    var hasOwn = {}.hasOwnProperty;
+
+    function classNames () {
+        var classes = [];
+
+        for (var i = 0; i < arguments.length; i++) {
+            var arg = arguments[i];
+            if (!arg) continue;
+
+            var argType = typeof arg;
+
+            if (argType === 'string' || argType === 'number') {
+                classes.push(arg);
+            } else if (Array.isArray(arg)) {
+                classes.push(classNames.apply(null, arg));
+            } else if (argType === 'object') {
+                for (var key in arg) {
+                    if (hasOwn.call(arg, key) && arg[key]) {
+                        classes.push(key);
+                    }
+                }
+            }
+        }
+
+        return classes.join(' ');
+    }
+
+
     angular.module('onosUtil')
         .factory('FnService',
         ['$window', '$location', '$log', function (_$window_, $loc, _$log_) {
@@ -423,7 +451,8 @@
                 parseBitRate: parseBitRate,
                 addToTrie: addToTrie,
                 removeFromTrie: removeFromTrie,
-                trieLookup: trieLookup
+                trieLookup: trieLookup,
+                classNames: classNames
             };
     }]);
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index da7d729..5b669ce 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -320,6 +320,7 @@
     // updateLinks - subfunctions
 
     function linkEntering(d) {
+
         var link = d3.select(this);
         d.el = link;
         api.restyleLinkElement(d);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index b3fc5a3..ef9f633 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -103,6 +103,7 @@
     // === EVENT HANDLERS
 
     function addDevice(data) {
+        console.log(data);
         var id = data.id,
             d;
 
@@ -1044,7 +1045,7 @@
         updateLinks();
         updateNodes();
     }
-    
+
     angular.module('ovTopo')
     .factory('TopoForceService',
         ['$log', '$timeout', 'FnService', 'SvgUtilService',
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
index 63b5dfb..3bdb8b2 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2-theme.css
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-
 /*
  ONOS GUI -- Topology View (theme) -- CSS file
  */
@@ -22,8 +21,7 @@
 /* --- Base SVG Layer --- */
 
 #ov-topo2 svg {
-    /*background-color: #f4f4f4;*/
-    background-color: goldenrod; /* just for testing */
+    background-color: #f4f4f4;
 }
 
 /* --- "No Devices" Layer --- */
@@ -32,15 +30,355 @@
     fill: #db7773;
 }
 
-#ov-topo2 svg #topo2-noDevsLayer text {
+#ov-topo2 svg #topo-noDevsLayer text {
     fill: #7e9aa8;
 }
 
 /* --- Topo Map --- */
 
-#ov-topo2 svg #topo2-map {
+#ov-topo2 svg #topo-map {
     stroke-width: 2px;
     stroke: #f4f4f4;
     fill: #e5e5e6;
 }
 
+/* --- general topo-panel styling --- */
+
+.topo-p svg {
+    background: #c0242b;
+}
+
+.topo-p svg .glyph {
+    fill: #ffffff;
+}
+
+.topo-p hr {
+    background-color: #cccccc;
+}
+
+#topo-p-detail svg {
+    background: none;
+}
+
+#topo-p-detail .header svg .glyph {
+    fill: #c0242b;
+}
+
+
+/* --- Topo Instance Panel --- */
+
+#topo-p-instance svg rect {
+    stroke-width: 0;
+    fill: #fbfbfb;
+}
+
+/* body of an instance */
+#topo-p-instance .online svg rect {
+    opacity: 1;
+    fill: #fbfbfb;
+}
+
+#topo-p-instance svg .glyph {
+    fill: #fff;
+}
+#topo-p-instance .online svg .glyph {
+    fill: #fff;
+}
+
+
+/* offline */
+#topo-p-instance svg .badgeIcon {
+    opacity: 0.4;
+    fill: #939598;
+}
+
+/* online */
+#topo-p-instance .online svg .badgeIcon {
+    opacity: 1.0;
+    fill: #939598;
+}
+#topo-p-instance .online svg .badgeIcon.bird {
+    fill: #ffffff;
+}
+
+#topo-p-instance svg .readyBadge {
+    visibility: hidden;
+}
+#topo-p-instance .ready svg .readyBadge {
+    visibility: visible;
+}
+
+#topo-p-instance svg text {
+    text-anchor: left;
+    opacity: 0.5;
+    fill: #3c3a3a;
+}
+
+#topo-p-instance .online svg text {
+    opacity: 1.0;
+    fill: #3c3a3a;
+}
+
+#topo-p-instance .onosInst.mastership {
+    opacity: 0.3;
+}
+#topo-p-instance .onosInst.mastership.affinity {
+    opacity: 1.0;
+}
+#topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url(#blue-glow);
+}
+
+.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+    filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  0 0 0 0 0  0 0 0 0 0  0.7 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
+}
+
+/* --- Topo Nodes --- */
+
+#ov-topo2 svg .suppressed {
+    opacity: 0.5 !important;
+}
+
+#ov-topo2 svg .suppressedmax {
+    opacity: 0.2 !important;
+}
+
+/* Device Nodes */
+
+/* note: device without the 'online' class is offline */
+#ov-topo2 svg .node.device rect {
+    /* TODO: theme */
+    fill: #f0f0f0;
+}
+#ov-topo2 svg .node.device text {
+    /*TODO: theme*/
+    fill: #bbb;
+}
+#ov-topo2 svg .node.device use {
+    /*TODO: theme*/
+    fill: #777;
+}
+
+
+#ov-topo2 svg .node.device.online rect {
+    fill: #ffffff;
+}
+#ov-topo2 svg .node.device.online text {
+    fill: #3c3a3a;
+}
+#ov-topo2 svg .node.device.online use {
+    /* NOTE: this gets overridden programatically */
+    fill: #454545;
+}
+
+
+#ov-topo2 svg .node.device.selected rect {
+    stroke-width: 2.0;
+    stroke: #009fdb;
+}
+
+/* Badges */
+/* (... works for bothand dark themes...) */
+#ov-topo2 svg .node .badge circle {
+    stroke: #aaa;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo circle {
+    fill: #99d;
+}
+
+#ov-topo2 svg .node .badge.badgeWarn circle {
+    fill: #da2;
+}
+
+#ov-topo2 svg .node .badge.badgeError circle {
+    fill: #e44;
+}
+
+#ov-topo2 svg .node .badge use {
+    fill: white !important;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo use {
+    fill: #448;
+}
+
+#ov-topo2 svg .node .badge text {
+    fill: white !important;
+}
+
+#ov-topo2 svg .node .badge.badgeInfo text {
+    fill: #448;
+}
+
+/* Host Nodes */
+
+#ov-topo2 svg .node.host {
+}
+
+#ov-topo2 svg .node.host text {
+    stroke: none;
+    font: 9pt sans-serif;
+    fill: #846;
+}
+
+#ov-topo2 svg .node.host circle {
+    stroke: #a3a596;
+    fill: #e0dfd6;
+}
+#ov-topo2 svg .node.host.selected .hostIcon > circle {
+    stroke-width: 2.0;
+    stroke: #009fdb;
+}
+
+#ov-topo2 svg .node.host use {
+    fill: #3c3a3a;
+}
+
+/* --- Topo Links --- */
+
+#ov-topo2 svg .link {
+    opacity: .9;
+}
+
+#ov-topo2 svg .link.selected,
+#ov-topo2 svg .link.enhanced {
+    stroke-width: 3.5;
+    stroke: #009fdb;
+}
+
+#ov-topo2 svg .link.inactive {
+    opacity: .5;
+    stroke-dasharray: 8 4;
+}
+/* TODO: Review for not-permitted links */
+#ov-topo2 svg .link.not-permitted {
+    stroke: rgb(255,0,0);
+    stroke-width: 5.0;
+    stroke-dasharray: 8 4;
+}
+
+#ov-topo2 svg .link.secondary {
+    stroke-width: 3px;
+    stroke: rgba(0,153,51,0.5);
+}
+
+/* Port traffic color visualization for Kbps, Mbps, and Gbps */
+
+#ov-topo2 svg .link.secondary.port-traffic-Kbps {
+    stroke: rgb(0,153,51);
+    stroke-width: 5.0;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Mbps {
+    stroke: rgb(128,145,27);
+    stroke-width: 6.5;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Gbps {
+    stroke: rgb(255, 137, 3);
+    stroke-width: 8.0;
+}
+
+#ov-topo2 svg .link.secondary.port-traffic-Gbps-choked {
+    stroke: rgb(183, 30, 21);
+    stroke-width: 8.0;
+}
+
+
+
+#ov-topo2 svg .link.animated {
+    stroke-dasharray: 8 5;
+    animation: ants 5s infinite linear;
+    /* below line could be added via Javascript, based on path, if we cared
+     * enough about the direction of ant-flow
+     */
+    /*animation-direction: reverse;*/
+}
+@keyframes ants {
+    from {
+        stroke-dashoffset: 0;
+    }
+    to {
+        stroke-dashoffset: 400;
+    }
+}
+
+#ov-topo2 svg .link.primary {
+    stroke-width: 4px;
+    stroke: #ffA300;
+}
+
+#ov-topo2 svg .link.secondary.optical {
+    stroke-width: 4px;
+    stroke: rgba(128,64,255,0.5);
+}
+
+#ov-topo2 svg .link.primary.optical {
+    stroke-width: 6px;
+    stroke: #74f;
+}
+
+/* Link Labels */
+#ov-topo2 svg .linkLabel rect {
+    stroke: none;
+    fill: #ffffff;
+}
+
+#ov-topo2 svg .linkLabel text {
+    fill: #444;
+}
+
+/* Port Labels */
+
+#ov-topo2 svg .portLabel rect {
+    stroke: #a3a596;
+    fill: #ffffff;
+}
+
+#ov-topo2 svg .portLabel text {
+    fill: #444;
+}
+
+/* Number of Links Labels */
+
+
+#ov-topo2 text.numLinkText {
+    fill: #444;
+}
+
+/* ------------------------------------------------- */
+/* Sprite Layer */
+
+#ov-topo2 svg #topo-sprites .gold1 use {
+    stroke: #fda;
+    fill: none;
+}
+#ov-topo2 svg #topo-sprites .gold1 text {
+    fill: #eda;
+}
+
+#ov-topo2 svg #topo-sprites .blue1 use {
+    stroke: #bbd;
+    fill: none;
+}
+#ov-topo2 svg #topo-sprites .blue1 text {
+    fill: #cce;
+}
+
+#ov-topo2 svg #topo-sprites .gray1 use {
+    stroke: #ccc;
+    fill: none;
+}
+#ov-topo2 svg #topo-sprites .gray1 text {
+    fill: #ddd;
+}
+
+/* fills */
+#ov-topo2 svg #topo-sprites use.fill-gray2 {
+    fill: #eee;
+}
+
+#ov-topo2 svg #topo-sprites use.fill-blue2 {
+    fill: #bce;
+}
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.html b/web/gui/src/main/webapp/app/view/topo2/topo2.html
index 1f0c6d6..32913a9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.html
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.html
@@ -1,6 +1,7 @@
 <!-- Topology View partial HTML -->
 <div id="ov-topo2">
-    <div id="topo2tmp">
+
+    <!-- <div id="topo2tmp">
         <div class="parentRegion">
             Parent Region: <span> - </span>
         </div>
@@ -27,7 +28,7 @@
             <h4>Peers</h4>
             <div></div>
         </div>
-    </div>
+    </div> -->
 
     <!-- Below here is good; Above here is temporary, for debugging -->
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.js b/web/gui/src/main/webapp/app/view/topo2/topo2.js
index 0631299..f62bf5c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -48,6 +48,7 @@
     // callback invoked when the SVG view has been resized..
     function svgResized(s) {
         $log.debug('topo2 view resized', s);
+        t2fs.newDim([s.width, s.height]);
     }
 
     function setUpKeys(overlayKeys) {
@@ -68,7 +69,7 @@
         ps.setPrefs('topo_zoom', {tx:tr[0], ty:tr[1], sc:sc});
 
         // keep the map lines constant width while zooming
-        mapG.style('stroke-width', (2.0 / sc) + 'px');
+//        mapG.style('stroke-width', (2.0 / sc) + 'px');
     }
 
     function setUpZoom() {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
index 3116a6f..e0aefb7 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -55,8 +55,6 @@
                     _this._byId[d.id] = model;
                 });
             }
-
-//            this.sort();
         },
         get: function (id) {
             if (!id) {
@@ -77,7 +75,10 @@
         _reset: function () {
             this._byId = [];
             this.models = [];
-        }
+        },
+        toJSON: function(options) {
+            return this.models.map(function(model) { return model.toJSON(options); });
+        },
     };
 
     Collection.extend = function (protoProps, staticProps) {
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2D3.js b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
new file mode 100644
index 0000000..604c907
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2D3.js
@@ -0,0 +1,163 @@
+/*
+* Copyright 2016-present Open Networking Laboratory
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+/*
+ONOS GUI -- Topology Layout Module.
+Module that contains the d3.force.layout logic
+*/
+
+(function () {
+    'use strict';
+
+    var sus, is, ts;
+
+    // internal state
+    var deviceLabelIndex = 0,
+    hostLabelIndex = 0;
+
+    // configuration
+    var devIconDim = 36,
+        labelPad = 4,
+        hostRadius = 14,
+        badgeConfig = {
+            radius: 12,
+            yoff: 5,
+            gdelta: 10
+        },
+        halfDevIcon = devIconDim / 2,
+        devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
+        hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
+        status = {
+            i: 'badgeInfo',
+            w: 'badgeWarn',
+            e: 'badgeError'
+        };
+
+    // note: these are the device icon colors without affinity (no master)
+    var dColTheme = {
+        light: {
+            online: '#444444',
+            offline: '#cccccc'
+        },
+        dark: {
+            // TODO: theme
+            online: '#444444',
+            offline: '#cccccc'
+        }
+    };
+
+    function init() {}
+
+    function renderBadge(node, bdg, boff) {
+        var bsel,
+            bcr = badgeConfig.radius,
+            bcgd = badgeConfig.gdelta;
+
+        node.select('g.badge').remove();
+
+        bsel = node.append('g')
+            .classed('badge', true)
+            .classed(badgeStatus(bdg), true)
+            .attr('transform', sus.translate(boff.dx, boff.dy));
+
+        bsel.append('circle')
+            .attr('r', bcr);
+
+        if (bdg.txt) {
+            bsel.append('text')
+                .attr('dy', badgeConfig.yoff)
+                .attr('text-anchor', 'middle')
+                .text(bdg.txt);
+        } else if (bdg.gid) {
+            bsel.append('use')
+                .attr({
+                    width: bcgd * 2,
+                    height: bcgd * 2,
+                    transform: sus.translate(-bcgd, -bcgd),
+                    'xlink:href': '#' + bdg.gid
+                });
+        }
+    }
+
+    // TODO: Move to Device Model when working on the Exit Devices
+    function updateDeviceRendering(d) {
+        var node = d.el,
+            bdg = d.badge,
+            label = trimLabel(deviceLabel(d)),
+            labelWidth;
+
+        node.select('text').text(label);
+        labelWidth = label ? computeLabelWidth(node) : 0;
+
+        node.select('rect')
+            .transition()
+            .attr(iconBox(devIconDim, labelWidth));
+
+        if (bdg) {
+            renderBadge(node, bdg, devBadgeOff);
+        }
+    }
+
+    function deviceEnter(device) {
+        device.onEnter(this, device);
+    }
+
+    function hostLabel(d) {
+        return d.get('id');
+
+        // var idx = (hostLabelIndex < d.get('labels').length) ? hostLabelIndex : 0;
+        // return d.labels[idx];
+    }
+
+    function hostEnter(d) {
+        var node = d3.select(this),
+            gid = d.get('type') || 'unknown',
+            textDy = hostRadius + 10;
+
+        d.el = node;
+        // sus.visible(node, api.showHosts());
+
+        is.addHostIcon(node, hostRadius, gid);
+
+        node.append('text')
+            .text(hostLabel)
+            .attr('dy', textDy)
+            .attr('text-anchor', 'middle');
+    }
+
+    function linkEntering(link) {
+        link.onEnter(this);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2D3Service',
+    ['SvgUtilService', 'IconService', 'ThemeService',
+
+        function (_sus_, _is_, _ts_) {
+            sus = _sus_;
+            is = _is_;
+            ts = _ts_;
+
+            return {
+                init: init,
+                deviceEnter: deviceEnter,
+                hostEnter: hostEnter,
+                linkEntering: linkEntering
+            }
+        }
+    ]
+);
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
index ae04111..88bf086 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -22,16 +22,37 @@
 (function () {
     'use strict';
 
-    var Collection, Model;
+    var Collection, Model, is, sus, ts, t2vs;
+
+    var remappedDeviceTypes = {
+        virtual: 'cord'
+    };
+
+    // configuration
+    var devIconDim = 36,
+        labelPad = 10,
+        hostRadius = 14,
+        badgeConfig = {
+            radius: 12,
+            yoff: 5,
+            gdelta: 10
+        },
+        halfDevIcon = devIconDim / 2,
+        devBadgeOff = { dx: -halfDevIcon, dy: -halfDevIcon },
+        hostBadgeOff = { dx: -hostRadius, dy: -hostRadius },
+        status = {
+            i: 'badgeInfo',
+            w: 'badgeWarn',
+            e: 'badgeError'
+        },
+        deviceLabelIndex = 0;
 
     function createDeviceCollection(data, region) {
 
         var DeviceCollection = Collection.extend({
             model: Model,
-            get: function () {},
             comparator: function(a, b) {
-
-                var order = region.layerOrder;
+                var order = region.get('layerOrder');
                 return order.indexOf(a.get('layer')) - order.indexOf(b.get('layer'));
             }
         });
@@ -49,14 +70,106 @@
         return deviceCollection;
     }
 
+    function mapDeviceTypeToGlyph(type) {
+        return remappedDeviceTypes[type] || type || 'unknown';
+    }
+
+    function deviceLabel(d) {
+        //TODO: Device Json is missing labels array
+        return "";
+        var labels = this.get('labels'),
+            idx = (deviceLabelIndex < labels.length) ? deviceLabelIndex : 0;
+        return labels[idx];
+    }
+
+    function trimLabel(label) {
+        return (label && label.trim()) || '';
+    }
+
+    function computeLabelWidth() {
+        var text = this.select('text'),
+        box = text.node().getBBox();
+        return box.width + labelPad * 2;
+    }
+
+    function iconBox(dim, labelWidth) {
+        return {
+            x: -dim / 2,
+            y: -dim / 2,
+            width: dim + labelWidth,
+            height: dim
+        }
+    }
+
+    function deviceGlyphColor(d) {
+
+        var o = this.node.online,
+            id = "127.0.0.1", // TODO: This should be from node.master
+            otag = o ? 'online' : 'offline';
+        return o ? sus.cat7().getColor(id, 0, ts.theme())
+                 : dColTheme[ts.theme()][otag];
+    }
+
+    function setDeviceColor() {
+        this.el.select('use')
+            .style('fill', this.deviceGlyphColor());
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2DeviceService',
-        ['Topo2Collection', 'Topo2Model',
+        ['Topo2Collection', 'Topo2NodeModel', 'IconService', 'SvgUtilService',
+        'ThemeService', 'Topo2ViewService',
 
-            function (_Collection_, _Model_) {
+            function (_Collection_, _NodeModel_, _is_, _sus_, _ts_, classnames, _t2vs_) {
 
+                t2vs = _t2vs_;
+                is = _is_;
+                sus = _sus_;
+                ts = _ts_;
                 Collection = _Collection_;
-                Model = _Model_.extend({});
+
+                Model = _NodeModel_.extend({
+                    initialize: function () {
+                        this.set('weight', 0);
+                        this.constructor.__super__.initialize.apply(this, arguments);
+                    },
+                    nodeType: 'device',
+                    deviceLabel: deviceLabel,
+                    deviceGlyphColor: deviceGlyphColor,
+                    mapDeviceTypeToGlyph: mapDeviceTypeToGlyph,
+                    trimLabel: trimLabel,
+                    setDeviceColor: setDeviceColor,
+                    onEnter: function (el) {
+
+                        var node = d3.select(el),
+                            glyphId = mapDeviceTypeToGlyph(this.get('type')),
+                            label = trimLabel(this.deviceLabel()),
+                            rect, text, glyph, labelWidth;
+
+                        this.el = node;
+
+                        rect = node.append('rect');
+
+                        text = node.append('text').text(label)
+                            .attr('text-anchor', 'left')
+                            .attr('y', '0.3em')
+                            .attr('x', halfDevIcon + labelPad);
+
+                        glyph = is.addDeviceIcon(node, glyphId, devIconDim);
+
+                        labelWidth = label ? computeLabelWidth(node) : 0;
+
+                        rect.attr(iconBox(devIconDim, labelWidth));
+                        glyph.attr(iconBox(devIconDim, 0));
+
+                        node.attr('transform', sus.translate(-halfDevIcon, -halfDevIcon));
+                        this.render();
+                    },
+                    onExit: function () {},
+                    render: function () {
+                        this.setDeviceColor();
+                    }
+                });
 
                 return {
                     createDeviceCollection: createDeviceCollection
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
index 481b96b..bbd7f8d 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -60,62 +60,17 @@
         linkLabel,
         node;
 
-    var $log, wss, t2is, t2rs;
+    var $log, wss, t2is, t2rs, t2ls, t2vs;
+    var svg, forceG, uplink, dim, opts;
 
     // ========================== Helper Functions
 
-    function init(_svg_, forceG, _uplink_, _dim_, opts) {
-
-        $log.debug('Initialize topo force layout');
-
-        nodeG = forceG.append('g').attr('id', 'topo-nodes');
-        node = nodeG.selectAll('.node');
-
-        linkG = forceG.append('g').attr('id', 'topo-links');
-        linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
-        numLinkLblsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
-        nodeG = forceG.append('g').attr('id', 'topo-nodes');
-        portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
-
-        link = linkG.selectAll('.link');
-        linkLabel = linkLabelG.selectAll('.linkLabel');
-        node = nodeG.selectAll('.node');
-
-        var width = 640,
-            height = 480;
-
-        var nodes = [
-            { x: width/3, y: height/2 },
-            { x: 2*width/3, y: height/2 }
-        ];
-
-        var links = [
-            { source: 0, target: 1 }
-        ];
-
-        var svg = d3.select('body').append('svg')
-            .attr('width', width)
-            .attr('height', height);
-
-        var force = d3.layout.force()
-            .size([width, height])
-            .nodes(nodes)
-            .links(links);
-
-        force.linkDistance(width/2);
-
-
-        var link = svg.selectAll('.link')
-            .data(links)
-            .enter().append('line')
-            .attr('class', 'link');
-
-        var node = svg.selectAll('.node')
-            .data(nodes)
-            .enter().append('circle')
-            .attr('class', 'node');
-
-        force.start();
+    function init(_svg_, _forceG_, _uplink_, _dim_, _opts_) {
+        svg = _svg_;
+        forceG = _forceG_;
+        uplink = _uplink_;
+        dim = _dim_;
+        opts = _opts_
     }
 
     function destroy() {
@@ -206,6 +161,9 @@
         $log.debug('>> topo2CurrentRegion event:', data);
         doTmpCurrentRegion(data);
         t2rs.addRegion(data);
+        t2ls.init(svg, forceG, uplink, dim, opts);
+        t2ls.update();
+        t2ls.start();
     }
 
     function topo2PeerRegions(data) {
@@ -257,20 +215,37 @@
         // link.classed(cls, b);
     }
 
+    function newDim(_dim_) {
+        dim = _dim_;
+        t2vs.newDim(dim);
+        // force.size(dim);
+        // tms.newDim(dim);
+        t2ls.setDimensions();
+    }
+
+    function getDim() {
+        return dim;
+    }
+
     // ========================== Main Service Definition
 
     angular.module('ovTopo2')
     .factory('Topo2ForceService',
         ['$log', 'WebSocketService', 'Topo2InstanceService', 'Topo2RegionService',
-        function (_$log_, _wss_, _t2is_, _t2rs_) {
+        'Topo2LayoutService', 'Topo2ViewService',
+        function (_$log_, _wss_, _t2is_, _t2rs_, _t2ls_, _t2vs_) {
+
             $log = _$log_;
             wss = _wss_;
             t2is = _t2is_;
             t2rs = _t2rs_;
+            t2ls = _t2ls_;
+            t2vs = _t2vs_;
 
             return {
 
                 init: init,
+                newDim: newDim,
 
                 destroy: destroy,
                 topo2AllInstances: allInstances,
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
index 19c2012..25d088a 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -22,7 +22,7 @@
 (function () {
     'use strict';
 
-    var Collection, Model;
+    var Collection, Model, t2vs;
 
     function createHostCollection(data, region) {
 
@@ -42,17 +42,21 @@
 
     angular.module('ovTopo2')
     .factory('Topo2HostService',
-        ['Topo2Collection', 'Topo2Model',
+    [
+        'Topo2Collection', 'Topo2NodeModel', 'Topo2ViewService',
+        function (_Collection_, _NodeModel_, classnames, _t2vs_) {
 
-            function (_Collection_, _Model_) {
+            t2vs = _t2vs_;
+            Collection = _Collection_;
 
-                Collection = _Collection_;
-                Model = _Model_.extend();
+            Model = _NodeModel_.extend({
+                nodeType: 'host'
+            });
 
-                return {
-                    createHostCollection: createHostCollection
-                };
-            }
-        ]);
+            return {
+                createHostCollection: createHostCollection
+            };
+        }
+    ]);
 
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
new file mode 100644
index 0000000..8cfaadf
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Layout.js
@@ -0,0 +1,334 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    var $log, sus, t2rs, t2d3, t2vs;
+
+    var linkG, linkLabelG, numLinkLabelsG, nodeG, portLabelG;
+    var link, linkLabel, node;
+
+    var nodes, links;
+
+    var force;
+
+    // default settings for force layout
+    var defaultSettings = {
+        gravity: 0.4,
+        friction: 0.7,
+        charge: {
+            // note: key is node.class
+            device: -8000,
+            host: -5000,
+            _def_: -12000
+        },
+        linkDistance: {
+            // note: key is link.type
+            direct: 100,
+            optical: 120,
+            hostLink: 3,
+            _def_: 50
+        },
+        linkStrength: {
+            // note: key is link.type
+            // range: {0.0 ... 1.0}
+            //direct: 1.0,
+            //optical: 1.0,
+            //hostLink: 1.0,
+            _def_: 1.0
+        }
+    };
+
+    // configuration
+    var linkConfig = {
+        light: {
+            baseColor: '#939598',
+            inColor: '#66f',
+            outColor: '#f00'
+        },
+        dark: {
+            // TODO : theme
+            baseColor: '#939598',
+            inColor: '#66f',
+            outColor: '#f00'
+        },
+        inWidth: 12,
+        outWidth: 10
+    };
+
+    // internal state
+    var settings,   // merged default settings and options
+        force,      // force layout object
+        drag,       // drag behavior handler
+        network = {
+            nodes: [],
+            links: [],
+            linksByDevice: {},
+            lookup: {},
+            revLinkToKey: {}
+        },
+        lu,                     // shorthand for lookup
+        rlk,                    // shorthand for revLinktoKey
+        showHosts = false,      // whether hosts are displayed
+        showOffline = true,     // whether offline devices are displayed
+        nodeLock = false,       // whether nodes can be dragged or not (locked)
+        fTimer,                 // timer for delayed force layout
+        fNodesTimer,            // timer for delayed nodes update
+        fLinksTimer,            // timer for delayed links update
+        dim,                    // the dimensions of the force layout [w,h]
+        linkNums = [];          // array of link number labels
+
+    var tickStuff = {
+        nodeAttr: {
+            transform: function (d) {
+                var dx = isNaN(d.x) ? 0 : d.x,
+                    dy = isNaN(d.y) ? 0 : d.y;
+                return sus.translate(dx, dy);
+            }
+        },
+        linkAttr: {
+            x1: function (d) { return d.get('position').x1; },
+            y1: function (d) { return d.get('position').y1; },
+            x2: function (d) { return d.get('position').x2; },
+            y2: function (d) { return d.get('position').y2; }
+        },
+        linkLabelAttr: {
+            transform: function (d) {
+                var lnk = tms.findLinkById(d.get('key'));
+                if (lnk) {
+                    return t2d3.transformLabel(lnk.get('position'));
+                }
+            }
+        }
+    };
+
+    function init(_svg_, forceG, _uplink_, _dim_, opts) {
+
+        $log.debug("Initialising Topology Layout");
+
+        settings = angular.extend({}, defaultSettings, opts);
+
+        linkG = forceG.append('g').attr('id', 'topo-links');
+        linkLabelG = forceG.append('g').attr('id', 'topo-linkLabels');
+        numLinkLabelsG = forceG.append('g').attr('id', 'topo-numLinkLabels');
+        nodeG = forceG.append('g').attr('id', 'topo-nodes');
+        portLabelG = forceG.append('g').attr('id', 'topo-portLabels');
+
+        link = linkG.selectAll('.link');
+        linkLabel = linkLabelG.selectAll('.linkLabel');
+        node = nodeG.selectAll('.node');
+
+        force = d3.layout.force()
+            .size(t2vs.getDimensions())
+            .nodes(t2rs.regionNodes())
+            .links(t2rs.regionLinks())
+            .gravity(settings.gravity)
+            .friction(settings.friction)
+            .charge(settings.charge._def_)
+            .linkDistance(settings.linkDistance._def_)
+            .linkStrength(settings.linkStrength._def_)
+            .on('tick', tick);
+    }
+
+    function tick() {
+        // guard against null (which can happen when our view pages out)...
+        if (node && node.size()) {
+            node.attr(tickStuff.nodeAttr);
+        }
+        if (link && link.size()) {
+            link.call(calcPosition)
+                .attr(tickStuff.linkAttr);
+            // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
+        }
+        if (linkLabel && linkLabel.size()) {
+            linkLabel.attr(tickStuff.linkLabelAttr);
+        }
+    }
+
+    function update() {
+        _updateNodes();
+        _updateLinks();
+    }
+
+    function _updateNodes() {
+
+        var regionNodes = t2rs.regionNodes();
+
+        // select all the nodes in the layout:
+        node = nodeG.selectAll('.node')
+            .data(regionNodes, function (d) { return d.get('id'); });
+
+        var entering = node.enter()
+            .append('g')
+            .attr({
+                id: function (d) { return sus.safeId(d.get('id')); },
+                class: function (d) { return d.svgClassName() },
+                transform: function (d) {
+                    // Need to guard against NaN here ??
+                    return sus.translate(d.node.x, d.node.y);
+                },
+                opacity: 0
+            })
+            // .on('mouseover', tss.nodeMouseOver)
+            // .on('mouseout', tss.nodeMouseOut)
+            .transition()
+            .attr('opacity', 1);
+
+        entering.filter('.device').each(t2d3.deviceEnter);
+        entering.filter('.host').each(t2d3.hostEnter);
+
+        // operate on both existing and new nodes:
+        // node.filter('.device').each(function (device) {
+        //     t2d3.updateDeviceColors(device);
+        // });
+    }
+
+    function _updateLinks() {
+
+        // var th = ts.theme();
+        var regionLinks = t2rs.regionLinks();
+
+        link = linkG.selectAll('.link')
+            .data(regionLinks, function (d) { return d.get('key'); });
+
+        // operate on existing links:
+        link.each(function (d) {
+            // this is supposed to be an existing link, but we have observed
+            //  occasions (where links are deleted and added rapidly?) where
+            //  the DOM element has not been defined. So protect against that...
+            if (d.el) {
+                restyleLinkElement(d, true);
+            }
+        });
+
+        // operate on entering links:
+        var entering = link.enter()
+            .append('line')
+            .call(calcPosition)
+            .attr({
+                x1: function (d) { return d.get('position').x1; },
+                y1: function (d) { return d.get('position').y1; },
+                x2: function (d) { return d.get('position').x2; },
+                y2: function (d) { return d.get('position').y2; },
+                stroke: linkConfig['light'].inColor,
+                'stroke-width': linkConfig.inWidth
+            });
+
+        entering.each(t2d3.linkEntering);
+
+        // operate on both existing and new links:
+        //link.each(...)
+
+        // add labels for how many links are in a thick line
+        // t2d3.applyNumLinkLabels(linkNums, numLinkLabelsG);
+
+        // apply or remove labels
+        // t2d3.applyLinkLabels();
+
+        // operate on exiting links:
+        link.exit()
+            .attr('stroke-dasharray', '3 3')
+            .attr('stroke', linkConfig['light'].outColor)
+            .style('opacity', 0.5)
+            .transition()
+            .duration(1500)
+            .attr({
+                'stroke-dasharray': '3 12',
+                'stroke-width': linkConfig.outWidth
+            })
+            .style('opacity', 0.0)
+            .remove();
+    }
+
+    function calcPosition() {
+        var lines = this,
+            linkSrcId,
+            linkNums = [];
+
+		lines.each(function (d) {
+            if (d.get('type') === 'hostLink') {
+                d.set('position', getDefaultPos(d));
+            }
+        });
+
+        function normalizeLinkSrc(link) {
+            // ensure source device is consistent across set of links
+            // temporary measure until link modeling is refactored
+            if (!linkSrcId) {
+                linkSrcId = link.source.id;
+                return false;
+            }
+
+            return link.source.id !== linkSrcId;
+        }
+
+        lines.each(function (d) {
+            d.set('position', getDefaultPos(d));
+        });
+    }
+
+	function getDefaultPos(link) {
+
+        return {
+            x1: link.get('source').x,
+            y1: link.get('source').y,
+            x2: link.get('target').x,
+            y2: link.get('target').y
+        };
+    }
+
+    function setDimensions() {
+        if (force) {
+            force.size(t2vs.getDimensions());
+        }
+    }
+
+
+    function start() {
+        force.start();
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2LayoutService',
+        [
+            '$log', 'SvgUtilService', 'Topo2RegionService',
+            'Topo2D3Service', 'Topo2ViewService',
+
+            function (_$log_, _sus_, _t2rs_, _t2d3_, _t2vs_) {
+
+                $log = _$log_;
+                t2rs = _t2rs_;
+                t2d3 = _t2d3_;
+                t2vs = _t2vs_;
+                sus = _sus_;
+
+                return {
+                    init: init,
+                    update: update,
+                    start: start,
+
+                    setDimensions: setDimensions
+                }
+            }
+        ]
+    );
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
index 5f2b6b7..44c5ec9 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -22,12 +22,162 @@
 (function () {
     'use strict';
 
-    var Collection, Model;
+    var Collection, Model, region, ts;
 
-    function createLinkCollection(data, region) {
+    var widthRatio = 1.4,
+        linkScale = d3.scale.linear()
+            .domain([1, 12])
+            .range([widthRatio, 12 * widthRatio])
+            .clamp(true),
+        allLinkTypes = 'direct indirect optical tunnel UiDeviceLink',
+        allLinkSubTypes = 'inactive not-permitted';
+
+    // configuration
+    var linkConfig = {
+        light: {
+            baseColor: '#939598',
+            inColor: '#66f',
+            outColor: '#f00'
+        },
+        dark: {
+            // TODO : theme
+            baseColor: '#939598',
+            inColor: '#66f',
+            outColor: '#f00'
+        },
+        inWidth: 12,
+        outWidth: 10
+    };
+
+    var defaultLinkType = 'direct',
+        nearDist = 15;
+
+    function createLink() {
+
+        var linkPoints = this.linkEndPoints(this.get('epA'), this.get('epB'));
+        console.log(this);
+
+        var attrs = angular.extend({}, linkPoints, {
+            key: this.get('id'),
+            class: 'link',
+            weight: 1,
+            srcPort: this.get('srcPort'),
+            tgtPort: this.get('dstPort'),
+            position: {
+                x1: 0,
+                y1: 0,
+                x2: 0,
+                y2: 0
+            }
+            // functions to aggregate dual link state
+//            extra: link.extra
+        });
+
+        this.set(attrs);
+    }
+
+    function linkEndPoints(srcId, dstId) {
+
+        var sourceNode = this.region.get('devices').get(srcId.substring(0, srcId.length -2));
+        var targetNode = this.region.get('devices').get(dstId.substring(0, dstId.length -2));
+
+//        var srcNode = lu[srcId],
+//            dstNode = lu[dstId],
+//            sMiss = !srcNode ? missMsg('src', srcId) : '',
+//            dMiss = !dstNode ? missMsg('dst', dstId) : '';
+//
+//        if (sMiss || dMiss) {
+//            $log.error('Node(s) not on map for link:' + sMiss + dMiss);
+//            //logicError('Node(s) not on map for link:\n' + sMiss + dMiss);
+//            return null;
+//        }
+
+        this.source = sourceNode.toJSON();
+        this.target = targetNode.toJSON();
+
+        return {
+            source: sourceNode,
+            target: targetNode
+        };
+    }
+
+    function createLinkCollection(data, _region) {
+
+        var LinkModel = Model.extend({
+            region: _region,
+            createLink: createLink,
+            linkEndPoints: linkEndPoints,
+            type: function () {
+                return this.get('type');
+            },
+            expected: function () {
+                //TODO: original code is: (s && s.expected) && (t && t.expected);
+                return true;
+            },
+            online: function () {
+                return true;
+                return both && (s && s.online) && (t && t.online);
+            },
+            linkWidth: function () {
+                var s = this.get('fromSource'),
+                    t = this.get('fromTarget'),
+                    ws = (s && s.linkWidth) || 0,
+                    wt = (t && t.linkWidth) || 0;
+
+                    // console.log(s);
+                // TODO: Current json is missing linkWidth
+                return 1.2;
+                return this.get('position').multiLink ? 5 : Math.max(ws, wt);
+            },
+
+            restyleLinkElement: function (immediate) {
+                // this fn's job is to look at raw links and decide what svg classes
+                // need to be applied to the line element in the DOM
+                var th = ts.theme(),
+                    el = this.el,
+                    type = this.get('type'),
+                    lw = this.linkWidth(),
+                    online = this.online(),
+                    modeCls = this.expected() ? 'inactive' : 'not-permitted',
+                    delay = immediate ? 0 : 1000;
+
+                console.log(type);
+
+                // NOTE: understand why el is sometimes undefined on addLink events...
+                // Investigated:
+                // el is undefined when it's a reverse link that is being added.
+                // updateLinks (which sets ldata.el) isn't called before this is called.
+                // Calling _updateLinks in addLinkUpdate fixes it, but there might be
+                // a more efficient way to fix it.
+                if (el && !el.empty()) {
+                    el.classed('link', true);
+                    el.classed(allLinkSubTypes, false);
+                    el.classed(modeCls, !online);
+                    el.classed(allLinkTypes, false);
+                    if (type) {
+                        el.classed(type, true);
+                    }
+                    el.transition()
+                        .duration(delay)
+                        .attr('stroke-width', linkScale(lw))
+                        .attr('stroke', linkConfig[th].baseColor);
+                }
+            },
+
+            onEnter: function (el) {
+                var link = d3.select(el);
+                this.el = link;
+
+                this.restyleLinkElement();
+
+                if (this.get('type') === 'hostLink') {
+                    sus.visible(link, api.showHosts());
+                }
+            }
+        });
 
         var LinkCollection = Collection.extend({
-            model: Model
+            model: LinkModel,
         });
 
         return new LinkCollection(data);
@@ -35,12 +185,13 @@
 
     angular.module('ovTopo2')
     .factory('Topo2LinkService',
-        ['Topo2Collection', 'Topo2Model',
+        ['Topo2Collection', 'Topo2Model', 'ThemeService',
 
-            function (_Collection_, _Model_) {
+            function (_Collection_, _Model_, _ts_) {
 
+                ts = _ts_;
                 Collection = _Collection_;
-                Model = _Model_.extend({});
+                Model = _Model_;
 
                 return {
                     createLinkCollection: createLinkCollection
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
index fa40d65..20fb5e0 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -1,23 +1,23 @@
 /*
- * Copyright 2016-present Open Networking Laboratory
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+* Copyright 2016-present Open Networking Laboratory
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
 
 /*
- ONOS GUI -- Topology Force Module.
- Visualization of the topology in an SVG layer, using a D3 Force Layout.
- */
+ONOS GUI -- Topology Force Module.
+Visualization of the topology in an SVG layer, using a D3 Force Layout.
+*/
 
 (function () {
     'use strict';
@@ -28,17 +28,86 @@
         this.attributes = {};
 
         attrs = angular.extend({}, attrs);
-        this.set(attrs);
+        this.set(attrs, { silent: true });
+        this.initialize.apply(this, arguments);
     }
 
     Model.prototype = {
 
+        initialize: function () {},
+
+        onChange: function (property, value, options) {},
+
         get: function (attr) {
             return this.attributes[attr];
         },
 
-        set: function(data) {
-            angular.extend(this.attributes, data);
+        set: function(key, val, options) {
+
+            if (!key) {
+                return this;
+            }
+
+            var attributes;
+            if (typeof key === 'object') {
+                attributes = key;
+                options = val;
+            } else {
+                (attributes = {})[key] = val;
+            }
+
+            options || (options = {});
+
+            var unset = options.unset,
+                silent = options.silent,
+                changes = [],
+                changing   = this._changing;
+
+            this._changing = true;
+
+            if (!changing) {
+
+                // NOTE: angular.copy causes issues in chrome
+                this._previousAttributes = Object.create(Object.getPrototypeOf(this.attributes));
+                this.changed = {};
+            }
+
+            var current = this.attributes,
+                changed = this.changed,
+                previous = this._previousAttributes;
+
+            angular.forEach(attributes, function (attribute, index) {
+
+                val = attribute;
+
+                if (!angular.equals(current[index], val)) {
+                    changes.push(index);
+                }
+
+                if (!angular.equals(previous[index], val)) {
+                    changed[index] = val;
+                } else {
+                    delete changed[index];
+                }
+
+                unset ? delete current[index] : current[index] = val;
+            });
+
+            // Trigger all relevant attribute changes.
+            if (!silent) {
+                if (changes.length) {
+                    this._pending = options;
+                }
+                for (var i = 0; i < changes.length; i++) {
+                    this.onChange(changes[i], this, current[changes[i]], options);
+                }
+            }
+
+            this._changing = false;
+            return this;
+        },
+        toJSON: function(options) {
+            return angular.copy(this.attributes)
         },
     };
 
@@ -67,11 +136,11 @@
     };
 
     angular.module('ovTopo2')
-        .factory('Topo2Model',
-        [
-            function () {
-                return Model;
-            }
-        ]);
+    .factory('Topo2Model',
+    [
+        function () {
+            return Model;
+        }
+    ]);
 
 })();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
new file mode 100644
index 0000000..54a2748
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2NodeModel.js
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    var randomService;
+    var fn;
+
+    //internal state;
+    var defaultLinkType = 'direct',
+        nearDist = 15;
+
+    function positionNode(node, forUpdate) {
+
+        var meta = node.metaUi,
+            x = meta && meta.x,
+            y = meta && meta.y,
+            dim = [800, 600],
+            xy;
+
+        // if the device contains explicit LONG/LAT data, use that to position
+        if (setLongLat(node)) {
+            //indicate we want to update cached meta data...
+            return true;
+        }
+
+        // else if we have [x,y] cached in meta data, use that...
+        if (x !== undefined && y !== undefined) {
+            node.fixed = true;
+            node.px = node.x = x;
+            node.py = node.y = y;
+            return;
+        }
+
+        // if this is a node update (not a node add).. skip randomizer
+        if (forUpdate) {
+            return;
+        }
+
+        // Note: Placing incoming unpinned nodes at exactly the same point
+        //        (center of the view) causes them to explode outwards when
+        //        the force layout kicks in. So, we spread them out a bit
+        //        initially, to provide a more serene layout convergence.
+        //       Additionally, if the node is a host, we place it near
+        //        the device it is connected to.
+
+        function rand() {
+            return {
+                x: randomService.randDim(dim[0]),
+                y: randomService.randDim(dim[1])
+            };
+        }
+
+        function near(node) {
+            return {
+                x: node.x + nearDist + randomService.spread(nearDist),
+                y: node.y + nearDist + randomService.spread(nearDist)
+            };
+        }
+
+        function getDevice(cp) {
+            // console.log(cp);
+            // var d = lu[cp.device];
+            // return d || rand();
+            return rand();
+        }
+
+        xy = (node.class === 'host') ? near(getDevice(node.cp)) : rand();
+        angular.extend(node, xy);
+    }
+
+    function setLongLat(node) {
+        var loc = node.location,
+            coord;
+
+        if (loc && loc.type === 'lnglat') {
+            coord = [0, 0];
+            node.fixed = true;
+            node.px = node.x = coord[0];
+            node.py = node.y = coord[1];
+            return true;
+        }
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2NodeModel',
+        ['Topo2Model', 'FnService',  'RandomService',
+        function (Model, _fn_, _RandomService_) {
+
+            randomService = _RandomService_;
+            fn = _fn_;
+
+            return Model.extend({
+                initialize: function () {
+                    this.node = this.createNode();
+                },
+                svgClassName: function () {
+                    return fn.classNames('node', this.nodeType, this.get('type'), {
+                        online: this.get('online')
+                    });
+                },
+                createNode: function () {
+
+                    var node = angular.extend({}, this.attributes);
+
+                    // Augment as needed...
+                    node.class = this.nodeType;
+                    node.svgClass = this.svgClassName();
+                    positionNode(node);
+                    return node;
+                }
+            });
+        }]
+    );
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
index ff1d52f..45c2652 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -24,12 +24,13 @@
 
     var $log,
         wss,
+        Model,
         t2sr,
         t2ds,
         t2hs,
         t2ls;
 
-    var regions;
+    var region;
 
     function init() {
         regions = {};
@@ -37,25 +38,46 @@
 
     function addRegion(data) {
 
-        var region = {
-            subregions: t2sr.createSubRegionCollection(data.subregions),
-            devices: t2ds.createDeviceCollection(data.devices, data),
-            hosts: t2hs.createHostCollection(data.hosts),
-            links: t2ls.createLinkCollection(data.links),
-        };
+        region = new Model({
+            id: data.id,
+            layerOrder: data.layerOrder
+        });
+
+        region.set({
+            subregions: t2sr.createSubRegionCollection(data.subregions, region),
+            devices: t2ds.createDeviceCollection(data.devices, region),
+            hosts: t2hs.createHostCollection(data.hosts, region),
+            links: t2ls.createLinkCollection(data.links, region),
+        });
+
+        region.set('test', 2);
+
+        angular.forEach(region.get('links').models, function (link) {
+            link.createLink();
+        });
 
         $log.debug('Region: ', region);
     }
 
+    function regionNodes() {
+        return [].concat(region.get('devices').models, region.get('hosts').models);
+    }
+
+
+    function regionLinks() {
+        return region.get('links').models;
+    }
+
     angular.module('ovTopo2')
     .factory('Topo2RegionService',
-        ['$log', 'WebSocketService', 'Topo2SubRegionService', 'Topo2DeviceService',
+        ['$log', 'WebSocketService', 'Topo2Model', 'Topo2SubRegionService', 'Topo2DeviceService',
         'Topo2HostService', 'Topo2LinkService',
 
-        function (_$log_, _wss_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
+        function (_$log_, _wss_, _Model_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
 
             $log = _$log_;
             wss = _wss_;
+            Model = _Model_
             t2sr = _t2sr_;
             t2ds = _t2ds_;
             t2hs = _t2hs_;
@@ -65,6 +87,9 @@
                 init: init,
 
                 addRegion: addRegion,
+                regionNodes: regionNodes,
+                regionLinks: regionLinks,
+
                 getSubRegions: t2sr.getSubRegions
             };
         }]);
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Select.js b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Select.js
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js b/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Theme.js
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2View.js b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
new file mode 100644
index 0000000..e856a1f
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2View.js
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ ONOS GUI -- Topology Layout Module.
+ Module that contains the d3.force.layout logic
+ */
+
+(function () {
+    'use strict';
+
+    var dimensions;
+
+    function newDim(_dimensions) {
+        dimensions = _dimensions;
+    }
+
+    function getDimensions() {
+        return dimensions;
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2ViewService',
+        [
+            function () {
+                return {
+                    newDim: newDim,
+                    getDimensions: getDimensions
+                }
+            }
+        ]
+    );
+})();
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 5f3cfb5..18e250f 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -128,15 +128,21 @@
     <!-- Under development for Region support. -->
     <script src="app/view/topo2/topo2.js"></script>
     <script src="app/view/topo2/topo2Collection.js"></script>
+    <script src="app/view/topo2/topo2D3.js"></script>
     <script src="app/view/topo2/topo2Device.js"></script>
-    <script src="app/view/topo2/topo2Model.js"></script>
     <script src="app/view/topo2/topo2Event.js"></script>
     <script src="app/view/topo2/topo2Force.js"></script>
     <script src="app/view/topo2/topo2Host.js"></script>
     <script src="app/view/topo2/topo2Instance.js"></script>
+    <script src="app/view/topo2/topo2Layout.js"></script>
     <script src="app/view/topo2/topo2Link.js"></script>
+    <script src="app/view/topo2/topo2Model.js"></script>
+    <script src="app/view/topo2/topo2NodeModel.js"></script>
     <script src="app/view/topo2/topo2Region.js"></script>
+    <script src="app/view/topo2/topo2Select.js"></script>
     <script src="app/view/topo2/topo2SubRegion.js"></script>
+    <script src="app/view/topo2/topo2Theme.js"></script>
+    <script src="app/view/topo2/topo2View.js"></script>
     <link rel="stylesheet" href="app/view/topo2/topo2.css">
     <link rel="stylesheet" href="app/view/topo2/topo2-theme.css">
 
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index 7e7dae5..e535460 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -216,7 +216,8 @@
             'isMobile', 'isChrome', 'isSafari', 'isFirefox',
             'debugOn', 'debug',
             'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
-            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup'
+            'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup',
+            'classNames'
         ])).toBeTruthy();
     });
 
diff --git a/web/gui/src/test/_karma/package.json b/web/gui/src/test/_karma/package.json
new file mode 100644
index 0000000..20042ec
--- /dev/null
+++ b/web/gui/src/test/_karma/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "karma",
+  "version": "1.0.0",
+  "description": "",
+  "main": "mockserver.js",
+  "dependencies": {
+    "websocket": "^1.0.23"
+  },
+  "devDependencies": {},
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC"
+}