Added Collections and Models for a Region.

Change-Id: Ic033b2890dad18e47b057e6b1d1c8535d812590d
diff --git a/.gitignore b/.gitignore
index 5817b7d..e109650 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,5 +22,7 @@
 /bin/
 
 web/gui/src/main/webapp/tests/node_modules
+web/gui/src/test/_karma/node_modules
 web/gui/src/main/webapp/node_modules/
+
 npm-debug.log
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 ce90af6..0631299 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.js
@@ -1,18 +1,18 @@
 /*
- * 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 View Module
@@ -28,7 +28,7 @@
         fs, mast, ks, zs,
         gs, ms, sus, flash,
         wss, ps, th,
-        t2es, t2fs;
+        t2es, t2fs, t2is;
 
     // DOM elements
     var ovtopo2, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
@@ -37,32 +37,66 @@
     var zoomer, actionMap;
 
 
-    // === Helper Functions
+    // --- Glyphs, Icons, and the like -----------------------------------
+
+    function setUpDefs() {
+        defs = svg.append('defs');
+        gs.loadDefs(defs);
+        sus.loadGlowDefs(defs);
+    }
 
     // callback invoked when the SVG view has been resized..
     function svgResized(s) {
-        $log.debug("topo2 view resized", s);
+        $log.debug('topo2 view resized', s);
     }
 
     function setUpKeys(overlayKeys) {
         $log.debug('topo2: set up keys....');
     }
 
+    // --- Pan and Zoom --------------------------------------------------
+
+    // zoom enabled predicate. ev is a D3 source event.
+    function zoomEnabled(ev) {
+        return fs.isMobile() || (ev.metaKey || ev.altKey);
+    }
+
+    function zoomCallback() {
+        var sc = zoomer.scale(),
+            tr = zoomer.translate();
+
+        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');
+    }
+
+    function setUpZoom() {
+        zoomLayer = svg.append('g').attr('id', 'topo-zoomlayer');
+        zoomer = zs.createZoomer({
+            svg: svg,
+            zoomLayer: zoomLayer,
+            zoomEnabled: zoomEnabled,
+            zoomCallback: zoomCallback
+        });
+    }
+
+
     // === Controller Definition -----------------------------------------
 
     angular.module('ovTopo2', ['onosUtil', 'onosSvg', 'onosRemote'])
-    .controller('OvTopo2Ctrl', 
+    .controller('OvTopo2Ctrl',
         ['$scope', '$log', '$location',
         'FnService', 'MastService', 'KeyService', 'ZoomService',
         'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
         'WebSocketService', 'PrefsService', 'ThemeService',
-        'Topo2EventService', 'Topo2ForceService',
+        'Topo2EventService', 'Topo2ForceService', 'Topo2InstanceService',
 
         function (_$scope_, _$log_, _$loc_,
-                  _fs_, _mast_, _ks_, _zs_,
-                  _gs_, _ms_, _sus_, _flash_,
-                  _wss_, _ps_, _th_,
-                  _t2es_, _t2fs_) {
+            _fs_, _mast_, _ks_, _zs_,
+            _gs_, _ms_, _sus_, _flash_,
+            _wss_, _ps_, _th_,
+            _t2es_, _t2fs_, _t2is_) {
 
             var params = _$loc_.search(),
                 projection,
@@ -95,9 +129,10 @@
             wss = _wss_;
             ps = _ps_;
             th = _th_;
-            
+
             t2es = _t2es_;
             t2fs = _t2fs_;
+            t2is = _t2is_;
 
             // capture selected intent parameters (if they are set in the
             //  query string) so that the traffic overlay can highlight
@@ -134,24 +169,32 @@
 
             // set up our keyboard shortcut bindings
             setUpKeys();
+            setUpZoom();
+            setUpDefs();
 
             // make sure we can respond to topology events from the server
             t2es.bindHandlers();
 
             // initialize the force layout, ready to render the topology
-            t2fs.init();
+            forceG = zoomLayer.append('g').attr('id', 'topo-force');
+            t2fs.init(svg, forceG, uplink, dim);
 
 
             // =-=-=-=-=-=-=-=-
             // TODO: in future, we will load background map data
-            //  asynchronously (hence the promise) and then chain off 
+            //  asynchronously (hence the promise) and then chain off
             //  there to send the topo2start event to the server.
             // For now, we'll send the event inline...
             t2es.start();
 
-            
+
+
+            t2is.initInst({ showMastership: t2fs.showMastership });
+
+
+
             // === ORIGINAL CODE ===
-            
+
             // setUpKeys();
             // setUpToolbar();
             // setUpDefs();
@@ -192,7 +235,7 @@
             // ttbs.setDefaultOverlay(prefsState.ovidx);
 
             // $log.debug('registered overlays...', tov.list());
-            
+
             $log.log('OvTopo2Ctrl has been created');
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
new file mode 100644
index 0000000..3116a6f
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Collection.js
@@ -0,0 +1,116 @@
+/*
+ * 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 Collection Module.
+ A Data Store that contains model data from the server
+ */
+
+(function () {
+    'use strict';
+
+    var Model;
+
+    function Collection(models, options) {
+
+        options || (options = {});
+
+        this.models = [];
+        this._reset();
+
+        if (options.comparator !== void 0) this.comparator = options.comparator;
+
+        if (models) {
+            this.add(models);
+        }
+    }
+
+    Collection.prototype = {
+        model: Model,
+        add: function (data) {
+
+            var _this = this;
+
+            if (angular.isArray(data)) {
+
+                data.forEach(function (d) {
+
+                    var model = new _this.model(d);
+                    model.collection = _this;
+
+                    _this.models.push(model);
+                    _this._byId[d.id] = model;
+                });
+            }
+
+//            this.sort();
+        },
+        get: function (id) {
+            if (!id) {
+                return void 0;
+            }
+            return this._byId[id] || null;
+        },
+        sort: function () {
+
+            var comparator = this.comparator;
+
+            // Check if function
+            comparator = comparator.bind(this);
+            this.models.sort(comparator);
+
+            return this;
+        },
+        _reset: function () {
+            this._byId = [];
+            this.models = [];
+        }
+    };
+
+    Collection.extend = function (protoProps, staticProps) {
+
+        var parent = this;
+        var child;
+
+        child = function () {
+            return parent.apply(this, arguments);
+        };
+
+        angular.extend(child, parent, staticProps);
+
+        // Set the prototype chain to inherit from `parent`, without calling
+        // `parent`'s constructor function and add the prototype properties.
+        child.prototype = angular.extend({}, parent.prototype, protoProps);
+        child.prototype.constructor = child;
+
+        // Set a convenience property in case the parent's prototype is needed
+        // later.
+        child.__super__ = parent.prototype;
+
+        return child;
+    };
+
+    angular.module('ovTopo2')
+        .factory('Topo2Collection',
+        ['Topo2Model',
+            function (_Model_) {
+
+                Model = _Model_;
+                return Collection;
+            }
+        ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Device.js b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
new file mode 100644
index 0000000..ae04111
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Device.js
@@ -0,0 +1,67 @@
+/*
+ * 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 Devices Module.
+ Module that holds the devices for a region
+ */
+
+(function () {
+    'use strict';
+
+    var Collection, Model;
+
+    function createDeviceCollection(data, region) {
+
+        var DeviceCollection = Collection.extend({
+            model: Model,
+            get: function () {},
+            comparator: function(a, b) {
+
+                var order = region.layerOrder;
+                return order.indexOf(a.get('layer')) - order.indexOf(b.get('layer'));
+            }
+        });
+
+        var devices = [];
+        data.forEach(function (deviceLayer) {
+            deviceLayer.forEach(function (device) {
+                devices.push(device);
+            });
+        });
+
+        var deviceCollection = new DeviceCollection(devices);
+        deviceCollection.sort();
+
+        return deviceCollection;
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2DeviceService',
+        ['Topo2Collection', 'Topo2Model',
+
+            function (_Collection_, _Model_) {
+
+                Collection = _Collection_;
+                Model = _Model_.extend({});
+
+                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 254f176..481b96b 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -23,12 +23,99 @@
     'use strict';
 
     // injected refs
-    var $log, wss;
+    var $log,
+        wss;
+
+    // SVG elements;
+    var linkG,
+        linkLabelG,
+        numLinkLblsG,
+        portLabelG,
+        nodeG;
+
+    // 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
+
+    // D3 selections;
+    var link,
+        linkLabel,
+        node;
+
+    var $log, wss, t2is, t2rs;
 
     // ========================== Helper Functions
 
-    function init() {
+    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 destroy() {
@@ -106,17 +193,19 @@
     // ========================== Event Handlers
 
     function allInstances(data) {
-        $log.debug('>> topo2AllInstances event:', data)
+        $log.debug('>> topo2AllInstances event:', data);
         doTmpCurrentLayout(data);
+        t2is.allInstances(data);
     }
 
     function currentLayout(data) {
-        $log.debug('>> topo2CurrentLayout event:', data)
+        $log.debug('>> topo2CurrentLayout event:', data);
     }
 
     function currentRegion(data) {
-        $log.debug('>> topo2CurrentRegion event:', data)
+        $log.debug('>> topo2CurrentRegion event:', data);
         doTmpCurrentRegion(data);
+        t2rs.addRegion(data);
     }
 
     function topo2PeerRegions(data) {
@@ -129,27 +218,68 @@
     }
 
     function startDone(data) {
-        $log.debug('>> topo2StartDone event:', data)
+        $log.debug('>> topo2StartDone event:', data);
     }
-    
+
+
+    function showMastership(masterId) {
+        if (!masterId) {
+            restoreLayerState();
+        } else {
+            showMastershipFor(masterId);
+        }
+    }
+
+    function restoreLayerState() {
+        // NOTE: this level of indirection required, for when we have
+        //          the layer filter functionality re-implemented
+        suppressLayers(false);
+    }
+
+    // ========================== Main Service Definition
+
+    function showMastershipFor(id) {
+        suppressLayers(true);
+        node.each(function (n) {
+            if (n.master === id) {
+                n.el.classed('suppressedmax', false);
+            }
+        });
+    }
+
+    function supAmt(less) {
+        return less ? 'suppressed' : 'suppressedmax';
+    }
+
+    function suppressLayers(b, less) {
+        var cls = supAmt(less);
+        node.classed(cls, b);
+        // link.classed(cls, b);
+    }
+
     // ========================== Main Service Definition
 
     angular.module('ovTopo2')
     .factory('Topo2ForceService',
-        ['$log', 'WebSocketService',
-
-        function (_$log_, _wss_) {
+        ['$log', 'WebSocketService', 'Topo2InstanceService', 'Topo2RegionService',
+        function (_$log_, _wss_, _t2is_, _t2rs_) {
             $log = _$log_;
             wss = _wss_;
-            
+            t2is = _t2is_;
+            t2rs = _t2rs_;
+
             return {
+
                 init: init,
+
                 destroy: destroy,
                 topo2AllInstances: allInstances,
                 topo2CurrentLayout: currentLayout,
                 topo2CurrentRegion: currentRegion,
-                topo2PeerRegions: topo2PeerRegions,
-                topo2StartDone: startDone
+                topo2StartDone: startDone,
+
+                showMastership: showMastership,
+                topo2PeerRegions: topo2PeerRegions
             };
         }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Host.js b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
new file mode 100644
index 0000000..19c2012
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Host.js
@@ -0,0 +1,58 @@
+/*
+ * 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 Hosts Module.
+ Module that holds the hosts for a region
+ */
+
+(function () {
+    'use strict';
+
+    var Collection, Model;
+
+    function createHostCollection(data, region) {
+
+        var HostCollection = Collection.extend({
+            model: Model
+        });
+
+        var hosts = [];
+        data.forEach(function (hostsLayer) {
+            hostsLayer.forEach(function (host) {
+                hosts.push(host);
+            });
+        });
+
+        return new HostCollection(hosts);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2HostService',
+        ['Topo2Collection', 'Topo2Model',
+
+            function (_Collection_, _Model_) {
+
+                Collection = _Collection_;
+                Model = _Model_.extend();
+
+                return {
+                    createHostCollection: createHostCollection
+                };
+            }
+        ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js b/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js
new file mode 100644
index 0000000..31c8886
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Instance.js
@@ -0,0 +1,301 @@
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log,
+        ps,
+        sus,
+        gs,
+        ts,
+        fs,
+        flash;
+
+    // api from topo
+    var api;
+
+    // configuration
+    var showLogicErrors = true,
+        idIns = 'topo-p-instance',
+        instOpts = {
+            edge: 'left',
+            width: 20
+        };
+
+    // internal state
+    var onosInstances,
+        onosOrder,
+        oiShowMaster,
+        oiBox;
+
+
+    function addInstance(data) {
+        var id = data.id;
+
+        if (onosInstances[id]) {
+            updateInstance(data);
+            return;
+        }
+        onosInstances[id] = data;
+        onosOrder.push(data);
+        updateInstances();
+    }
+
+    function updateInstance(data) {
+        var id = data.id,
+            d = onosInstances[id];
+        if (d) {
+            angular.extend(d, data);
+            updateInstances();
+        } else {
+            logicError('updateInstance: lookup fail: ID = "' + id + '"');
+        }
+    }
+
+    function removeInstance(data) {
+        var id = data.id,
+            d = onosInstances[id];
+        if (d) {
+            var idx = fs.find(id, onosOrder);
+            if (idx >= 0) {
+                onosOrder.splice(idx, 1);
+            }
+            delete onosInstances[id];
+            updateInstances();
+        } else {
+            logicError('removeInstance lookup fail. ID = "' + id + '"');
+        }
+    }
+
+    // ==========================
+
+    function clickInst(d) {
+        var el = d3.select(this),
+            aff = el.classed('affinity');
+        if (!aff) {
+            setAffinity(el, d);
+        } else {
+            cancelAffinity();
+        }
+    }
+
+    function setAffinity(el, d) {
+        d3.selectAll('.onosInst')
+            .classed('mastership', true)
+            .classed('affinity', false);
+        el.classed('affinity', true);
+
+        // suppress all elements except nodes whose master is this instance
+        api.showMastership(d.id);
+        oiShowMaster = true;
+    }
+
+    function cancelAffinity() {
+        d3.selectAll('.onosInst')
+            .classed('mastership affinity', false);
+
+        api.showMastership(null);
+        oiShowMaster = false;
+    }
+
+    function attachUiBadge(svg) {
+        gs.addGlyph(svg, 'uiAttached', 24, true, [14, 54])
+            .classed('badgeIcon uiBadge', true);
+    }
+
+    function attachReadyBadge(svg) {
+        gs.addGlyph(svg, 'checkMark', 16, true, [18, 40])
+            .classed('badgeIcon readyBadge', true);
+    }
+
+    function instColor(id, online) {
+        return sus.cat7().getColor(id, !online, ts.theme());
+    }
+
+    // ==============================
+
+    function updateInstances() {
+        var rox = 5,
+            roy = 5,
+            rw = 160,
+            rhh = 30,
+            rbh = 45,
+            tx = 48,
+            instSvg = {
+                width: 170,
+                height: 85,
+                viewBox: '0 0 170 85'
+            },
+            headRect = {
+                x: rox,
+                y: roy,
+                width: rw,
+                height: rhh
+            },
+            bodyRect = {
+                x: rox,
+                y: roy + rhh,
+                width: rw,
+                height: rbh
+            },
+            titleAttr = {
+                class: 'instTitle',
+                x: tx,
+                y: 27
+            };
+
+        var onoses = oiBox.el().selectAll('.onosInst')
+                .data(onosOrder, function (d) { return d.id; });
+
+        function nSw(n) {
+            return 'Devices: ' + n;
+        }
+
+        // operate on existing onos instances if necessary
+        onoses.each(function (d) {
+            var el = d3.select(this),
+                svg = el.select('svg');
+
+            // update online state
+            el.classed('online', d.online);
+            el.classed('ready', d.ready);
+
+            // update ui-attached state
+            svg.select('use.uiBadge').remove();
+            if (d.uiAttached) {
+                attachUiBadge(svg);
+            }
+
+            function updAttr(id, value) {
+                svg.select('text.instLabel.' + id).text(value);
+            }
+
+            updAttr('ip', d.ip);
+            updAttr('ns', nSw(d.switches));
+        });
+
+
+        // operate on new onos instances
+        var entering = onoses.enter()
+            .append('div')
+            .classed('onosInst', true)
+            .classed('online', function (d) { return d.online; })
+            .classed('ready', function (d) { return d.ready; })
+            .on('click', clickInst);
+
+        entering.each(function (d) {
+            var el = d3.select(this),
+                svg = el.append('svg').attr(instSvg);
+
+            svg.append('rect').attr(headRect);
+            svg.append('rect').attr(bodyRect);
+
+            gs.addGlyph(svg, 'bird', 20, false, [15, 10])
+                .classed('badgeIcon bird', true);
+
+            attachReadyBadge(svg);
+
+            if (d.uiAttached) {
+                attachUiBadge(svg);
+            }
+
+            svg.append('text')
+                .attr(titleAttr)
+                .text(d.id);
+
+            var ty = 55;
+            function addAttr(id, label) {
+                svg.append('text').attr({
+                    class: 'instLabel ' + id,
+                    x: tx,
+                    y: ty
+                }).text(label);
+                ty += 18;
+            }
+
+            addAttr('ip', d.ip);
+            addAttr('ns', nSw(d.switches));
+        });
+
+        // operate on existing + new onoses here
+        // set the affinity colors...
+        onoses.each(function (d) {
+
+            var el = d3.select(this),
+                rect = el.select('svg').select('rect'),
+                col = instColor(d.id, d.online);
+
+            rect.style('fill', col);
+        });
+
+        // adjust the panel size appropriately...
+        oiBox.width(instSvg.width * onosOrder.length);
+        oiBox.height(instSvg.height);
+
+        // remove any outgoing instances
+        onoses.exit().remove();
+    }
+
+
+    // ==========================
+
+    function logicError(msg) {
+        if (showLogicErrors) {
+            $log.warn('TopoInstService: ' + msg);
+        }
+    }
+
+    function initInst(_api_) {
+        api = _api_;
+        oiBox = ps.createPanel(idIns, instOpts);
+        oiBox.show();
+
+        onosInstances = {};
+        onosOrder = [];
+        oiShowMaster = false;
+
+        // we want to update the instances, each time the theme changes
+        ts.addListener(updateInstances);
+    }
+
+    function destroyInst() {
+        ts.removeListener(updateInstances);
+
+        ps.destroyPanel(idIns);
+        oiBox = null;
+
+        onosInstances = {};
+        onosOrder = [];
+        oiShowMaster = false;
+    }
+
+    function allInstances(data) {
+        $log.debug('Update all instances', data);
+
+        var members = data.members;
+
+        members.forEach(function (member) {
+            addInstance(member);
+        });
+    }
+
+    angular.module('ovTopo2')
+        .factory('Topo2InstanceService',
+        ['$log', 'PanelService', 'SvgUtilService', 'GlyphService',
+        'ThemeService', 'FnService', 'FlashService',
+
+        function (_$log_, _ps_, _sus_, _gs_, _ts_, _fs_, _flash_) {
+            $log = _$log_;
+            ps = _ps_;
+            sus = _sus_;
+            gs = _gs_;
+            ts = _ts_;
+            fs = _fs_;
+            flash = _flash_;
+
+            return {
+                initInst: initInst,
+                allInstances: allInstances
+            };
+        }]);
+
+}());
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Link.js b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
new file mode 100644
index 0000000..5f2b6b7
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Link.js
@@ -0,0 +1,51 @@
+/*
+ * 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 Links Module.
+ Module that holds the links for a region
+ */
+
+(function () {
+    'use strict';
+
+    var Collection, Model;
+
+    function createLinkCollection(data, region) {
+
+        var LinkCollection = Collection.extend({
+            model: Model
+        });
+
+        return new LinkCollection(data);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2LinkService',
+        ['Topo2Collection', 'Topo2Model',
+
+            function (_Collection_, _Model_) {
+
+                Collection = _Collection_;
+                Model = _Model_.extend({});
+
+                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
new file mode 100644
index 0000000..fa40d65
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Model.js
@@ -0,0 +1,77 @@
+/*
+ * 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.
+ */
+
+(function () {
+    'use strict';
+
+    function Model(attributes) {
+
+        var attrs = attributes || {};
+        this.attributes = {};
+
+        attrs = angular.extend({}, attrs);
+        this.set(attrs);
+    }
+
+    Model.prototype = {
+
+        get: function (attr) {
+            return this.attributes[attr];
+        },
+
+        set: function(data) {
+            angular.extend(this.attributes, data);
+        },
+    };
+
+
+    Model.extend = function (protoProps, staticProps) {
+
+        var parent = this;
+        var child;
+
+        child = function () {
+            return parent.apply(this, arguments);
+        };
+
+        angular.extend(child, parent, staticProps);
+
+        // Set the prototype chain to inherit from `parent`, without calling
+        // `parent`'s constructor function and add the prototype properties.
+        child.prototype = angular.extend({}, parent.prototype, protoProps);
+        child.prototype.constructor = child;
+
+        // Set a convenience property in case the parent's prototype is needed
+        // later.
+        child.__super__ = parent.prototype;
+
+        return child;
+    };
+
+    angular.module('ovTopo2')
+        .factory('Topo2Model',
+        [
+            function () {
+                return Model;
+            }
+        ]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Region.js b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
new file mode 100644
index 0000000..ff1d52f
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Region.js
@@ -0,0 +1,72 @@
+/*
+ * 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 Region Module.
+ Module that holds the current region in memory
+ */
+
+(function () {
+    'use strict';
+
+    var $log,
+        wss,
+        t2sr,
+        t2ds,
+        t2hs,
+        t2ls;
+
+    var regions;
+
+    function init() {
+        regions = {};
+    }
+
+    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),
+        };
+
+        $log.debug('Region: ', region);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2RegionService',
+        ['$log', 'WebSocketService', 'Topo2SubRegionService', 'Topo2DeviceService',
+        'Topo2HostService', 'Topo2LinkService',
+
+        function (_$log_, _wss_, _t2sr_, _t2ds_, _t2hs_, _t2ls_) {
+
+            $log = _$log_;
+            wss = _wss_;
+            t2sr = _t2sr_;
+            t2ds = _t2ds_;
+            t2hs = _t2hs_;
+            t2ls = _t2ls_;
+
+            return {
+                init: init,
+
+                addRegion: addRegion,
+                getSubRegions: t2sr.getSubRegions
+            };
+        }]);
+
+})();
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
new file mode 100644
index 0000000..421f57d
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2SubRegion.js
@@ -0,0 +1,51 @@
+/*
+ * 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 SubRegion Module.
+ Module that holds the sub-regions for a region
+ */
+
+(function () {
+    'use strict';
+
+    var Collection, Model;
+
+    function createSubRegionCollection(data, region) {
+
+        var SubRegionCollection = Collection.extend({
+            model: Model
+        });
+
+        return new SubRegionCollection(data);
+    }
+
+    angular.module('ovTopo2')
+    .factory('Topo2SubRegionService',
+        ['Topo2Collection', 'Topo2Model',
+
+            function (_Collection_, _Model_) {
+
+                Collection = _Collection_;
+                Model = _Model_.extend({});
+
+                return {
+                    createSubRegionCollection: createSubRegionCollection
+                };
+            }
+        ]);
+
+})();
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index da438ba..5f3cfb5 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -127,8 +127,16 @@
 
     <!-- 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/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/topo2Link.js"></script>
+    <script src="app/view/topo2/topo2Region.js"></script>
+    <script src="app/view/topo2/topo2SubRegion.js"></script>
     <link rel="stylesheet" href="app/view/topo2/topo2.css">
     <link rel="stylesheet" href="app/view/topo2/topo2-theme.css">