Merge remote-tracking branch 'origin/master'
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 95c4e4d..703a240 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -70,6 +70,7 @@
     <script type="text/javascript">
         var ONOS = $.onos({
             comment: "configuration options",
+            startVid: 'topo',
             trace: false
         });
     </script>
@@ -77,12 +78,15 @@
     <!-- Framework module files included here -->
     <script src="mast2.js"></script>
 
-    <!-- Contributed (application) views injected here -->
-    <!-- TODO: replace with template marker and inject refs server-side -->
+    <!-- Sample views; can be dispensed with eventually -->
     <script src="sample2.js"></script>
     <script src="sampleAlt2.js"></script>
     <script src="sampleRadio.js"></script>
 
+    <!-- Contributed (application) views injected here -->
+    <!-- TODO: replace with template marker and inject refs server-side -->
+    <script src="topo2.js"></script>
+
     <!-- finally, build the UI-->
     <script type="text/javascript">
         $(ONOS.buildUi);
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 85aa617..6353a6e 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -25,7 +25,7 @@
     var tsI = new Date().getTime(),         // initialize time stamp
         tsB,                                // build time stamp
         mastHeight = 36,                    // see mast2.css
-        defaultHash = 'sample';
+        defaultVid = 'sample';
 
 
     // attach our main function to the jQuery object
@@ -35,7 +35,8 @@
             navApi;
 
         var defaultOptions = {
-            trace: false
+            trace: false,
+            startVid: defaultVid
         };
 
         // compute runtime settings
@@ -91,7 +92,7 @@
             traceFn('hash', hash);
 
             if (!hash) {
-                hash = defaultHash;
+                hash = settings.startVid;
                 redo = true;
             }
 
@@ -336,10 +337,6 @@
         }
 
         var viewInstanceMethods = {
-            toString: function () {
-                return '[View: id="' + this.vid + '"]';
-            },
-
             token: function () {
                 return {
                     // attributes
@@ -350,6 +347,7 @@
                     // functions
                     width: this.width,
                     height: this.height,
+                    uid: this.uid,
                     setRadio: this.setRadio
                 }
             },
@@ -433,6 +431,10 @@
 
             setRadio: function (btnSet, cb) {
                 setRadioButtons(this.vid, btnSet, cb);
+            },
+
+            uid: function (id) {
+                return uid(this, id);
             }
 
             // TODO: consider schedule, clearTimer, etc.
diff --git a/web/gui/src/main/webapp/topo2-OLD.js b/web/gui/src/main/webapp/topo2-OLD.js
new file mode 100644
index 0000000..04ce7ab
--- /dev/null
+++ b/web/gui/src/main/webapp/topo2-OLD.js
@@ -0,0 +1,1219 @@
+/*
+ * Copyright 2014 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 network topology viewer - PoC version 1.0
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    // configuration data
+    var config = {
+        useLiveData: true,
+        debugOn: false,
+        debug: {
+            showNodeXY: false,
+            showKeyHandler: true
+        },
+        options: {
+            layering: true,
+            collisionPrevention: true,
+            loadBackground: true
+        },
+        backgroundUrl: 'img/us-map.png',
+        data: {
+            live: {
+                jsonUrl: 'rs/topology/graph',
+                detailPrefix: 'rs/topology/graph/',
+                detailSuffix: ''
+            },
+            fake: {
+                jsonUrl: 'json/network2.json',
+                detailPrefix: 'json/',
+                detailSuffix: '.json'
+            }
+        },
+        iconUrl: {
+            device: 'img/device.png',
+            host: 'img/host.png',
+            pkt: 'img/pkt.png',
+            opt: 'img/opt.png'
+        },
+        mastHeight: 36,
+        force: {
+            note: 'node.class or link.class is used to differentiate',
+            linkDistance: {
+                infra: 200,
+                host: 40
+            },
+            linkStrength: {
+                infra: 1.0,
+                host: 1.0
+            },
+            charge: {
+                device: -800,
+                host: -1000
+            },
+            ticksWithoutCollisions: 50,
+            marginLR: 20,
+            marginTB: 20,
+            translate: function() {
+                return 'translate(' +
+                    config.force.marginLR + ',' +
+                    config.force.marginTB + ')';
+            }
+        },
+        labels: {
+            imgPad: 16,
+            padLR: 8,
+            padTB: 6,
+            marginLR: 3,
+            marginTB: 2,
+            port: {
+                gap: 3,
+                width: 18,
+                height: 14
+            }
+        },
+        icons: {
+            w: 32,
+            h: 32,
+            xoff: -12,
+            yoff: -8
+        },
+        constraints: {
+            ypos: {
+                host: 0.05,
+                switch: 0.3,
+                roadm: 0.7
+            }
+        },
+        hostLinkWidth: 1.0,
+        hostRadius: 7,
+        mouseOutTimerDelayMs: 120
+    };
+
+    // state variables
+    var netView = {},
+        network = {},
+        selected = {},
+        highlighted = null,
+        hovered = null,
+        viewMode = 'showAll',
+        portLabelsOn = false;
+
+
+    function debug(what) {
+        return config.debugOn && config.debug[what];
+    }
+
+    function urlData() {
+        return config.data[config.useLiveData ? 'live' : 'fake'];
+    }
+
+    function networkJsonUrl() {
+        return urlData().jsonUrl;
+    }
+
+    function safeId(id) {
+        return id.replace(/[^a-z0-9]/gi, '_');
+    }
+
+    function detailJsonUrl(id) {
+        var u = urlData(),
+            encId = config.useLiveData ? encodeURIComponent(id) : safeId(id);
+        return u.detailPrefix + encId + u.detailSuffix;
+    }
+
+
+    // load the topology view of the network
+    function loadNetworkView() {
+        // Hey, here I am, calling something on the ONOS api:
+        api.printTime();
+
+        resize();
+
+        // go get our network data from the server...
+        var url = networkJsonUrl();
+        d3.json(url , function (err, data) {
+            if (err) {
+                alert('Oops! Error reading JSON...\n\n' +
+                    'URL: ' + url + '\n\n' +
+                    'Error: ' + err.message);
+                return;
+            }
+//            console.log("here is the JSON data...");
+//            console.log(data);
+
+            network.data = data;
+            drawNetwork();
+        });
+
+        // while we wait for the data, set up the handlers...
+        setUpClickHandler();
+        setUpRadioButtonHandler();
+        setUpKeyHandler();
+        $(window).on('resize', resize);
+    }
+
+    function setUpClickHandler() {
+        // click handler for "selectable" objects
+        $(document).on('click', '.select-object', function () {
+            // when any object of class "select-object" is clicked...
+            var obj = network.lookup[$(this).data('id')];
+            if (obj) {
+                selectObject(obj);
+            }
+            // stop propagation of event (I think) ...
+            return false;
+        });
+    }
+
+    function setUpRadioButtonHandler() {
+        d3.selectAll('#displayModes .radio').on('click', function () {
+            var id = d3.select(this).attr('id');
+            if (id !== viewMode) {
+                radioButton('displayModes', id);
+                viewMode = id;
+                doRadioAction(id);
+            }
+        });
+    }
+
+    function doRadioAction(id) {
+        showAllLayers();
+        if (id === 'showPkt') {
+            showPacketLayer();
+        } else if (id === 'showOpt') {
+            showOpticalLayer();
+        }
+    }
+
+    function showAllLayers() {
+        network.node.classed('inactive', false);
+        network.link.classed('inactive', false);
+        d3.selectAll('svg .port').classed('inactive', false);
+        d3.selectAll('svg .portText').classed('inactive', false);
+    }
+
+    function showPacketLayer() {
+        network.node.each(function(d) {
+            // deactivate nodes that are not hosts or switches
+            if (d.class === 'device' && d.type !== 'switch') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        network.link.each(function(lnk) {
+            // deactivate infrastructure links that have opt's as endpoints
+            if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        // deactivate non-packet ports
+        d3.selectAll('svg .optPort').classed('inactive', true)
+    }
+
+    function showOpticalLayer() {
+        network.node.each(function(d) {
+            // deactivate nodes that are not optical devices
+            if (d.type !== 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        network.link.each(function(lnk) {
+            // deactivate infrastructure links that have opt's as endpoints
+            if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        // deactivate non-packet ports
+        d3.selectAll('svg .pktPort').classed('inactive', true)
+    }
+
+    function setUpKeyHandler() {
+        d3.select('body')
+            .on('keydown', function () {
+                processKeyEvent();
+                if (debug('showKeyHandler')) {
+                    network.svg.append('text')
+                        .attr('x', 5)
+                        .attr('y', 15)
+                        .style('font-size', '20pt')
+                        .text('keyCode: ' + d3.event.keyCode +
+                            ' applied to : ' + contextLabel())
+                        .transition().duration(2000)
+                        .style('font-size', '2pt')
+                        .style('fill-opacity', 0.01)
+                        .remove();
+                }
+            });
+    }
+
+    function contextLabel() {
+        return hovered === null ? "(nothing)" : hovered.id;
+    }
+
+    function radioButton(group, id) {
+        d3.selectAll("#" + group + " .radio").classed("active", false);
+        d3.select("#" + group + " #" + id).classed("active", true);
+    }
+
+    function processKeyEvent() {
+        var code = d3.event.keyCode;
+        switch (code) {
+            case 66:    // B
+                toggleBackground();
+                break;
+            case 71:    // G
+                cycleLayout();
+                break;
+            case 76:    // L
+                cycleLabels();
+                break;
+            case 80:    // P
+                togglePorts();
+                break;
+            case 85:    // U
+                unpin();
+                break;
+        }
+
+    }
+
+    function toggleBackground() {
+        var bg = d3.select('#bg'),
+            vis = bg.style('visibility'),
+            newvis = (vis === 'hidden') ? 'visible' : 'hidden';
+        bg.style('visibility', newvis);
+    }
+
+    function cycleLayout() {
+        config.options.layering = !config.options.layering;
+        network.force.resume();
+    }
+
+    function cycleLabels() {
+        console.log('Cycle Labels - context = ' + contextLabel());
+    }
+
+    function togglePorts() {
+        portLabelsOn = !portLabelsOn;
+        var portVis = portLabelsOn ? 'visible' : 'hidden';
+        d3.selectAll('.port').style('visibility', portVis);
+        d3.selectAll('.portText').style('visibility', portVis);
+    }
+
+    function unpin() {
+        if (hovered) {
+            hovered.fixed = false;
+            findNodeFromData(hovered).classed('fixed', false);
+            network.force.resume();
+        }
+        console.log('Unpin - context = ' + contextLabel());
+    }
+
+
+    // ========================================================
+
+    function drawNetwork() {
+        $('#view').empty();
+
+        prepareNodesAndLinks();
+        createLayout();
+        console.log("\n\nHere is the augmented network object...");
+        console.log(network);
+    }
+
+    function prepareNodesAndLinks() {
+        network.lookup = {};
+        network.nodes = [];
+        network.links = [];
+
+        var nw = network.forceWidth,
+            nh = network.forceHeight;
+
+        function yPosConstraintForNode(n) {
+            return config.constraints.ypos[n.type || 'host'];
+        }
+
+        // Note that both 'devices' and 'hosts' get mapped into the nodes array
+
+        // first, the devices...
+        network.data.devices.forEach(function(n) {
+            var ypc = yPosConstraintForNode(n),
+                ix = Math.random() * 0.6 * nw + 0.2 * nw,
+                iy = ypc * nh,
+                node = {
+                    id: n.id,
+                    labels: n.labels,
+                    class: 'device',
+                    icon: 'device',
+                    type: n.type,
+                    x: ix,
+                    y: iy,
+                    constraint: {
+                        weight: 0.7,
+                        y: iy
+                    }
+                };
+            network.lookup[n.id] = node;
+            network.nodes.push(node);
+        });
+
+        // then, the hosts...
+        network.data.hosts.forEach(function(n) {
+            var ypc = yPosConstraintForNode(n),
+                ix = Math.random() * 0.6 * nw + 0.2 * nw,
+                iy = ypc * nh,
+                node = {
+                    id: n.id,
+                    labels: n.labels,
+                    class: 'host',
+                    icon: 'host',
+                    type: n.type,
+                    x: ix,
+                    y: iy,
+                    constraint: {
+                        weight: 0.7,
+                        y: iy
+                    }
+                };
+            network.lookup[n.id] = node;
+            network.nodes.push(node);
+        });
+
+
+        // now, process the explicit links...
+        network.data.links.forEach(function(lnk) {
+            var src = network.lookup[lnk.src],
+                dst = network.lookup[lnk.dst],
+                id = src.id + "-" + dst.id;
+
+            var link = {
+                class: 'infra',
+                id: id,
+                type: lnk.type,
+                width: lnk.linkWidth,
+                source: src,
+                srcPort: lnk.srcPort,
+                target: dst,
+                tgtPort: lnk.dstPort,
+                strength: config.force.linkStrength.infra
+            };
+            network.links.push(link);
+        });
+
+        // finally, infer host links...
+        network.data.hosts.forEach(function(n) {
+            var src = network.lookup[n.id],
+                dst = network.lookup[n.cp.device],
+                id = src.id + "-" + dst.id;
+
+            var link = {
+                class: 'host',
+                id: id,
+                type: 'hostLink',
+                width: config.hostLinkWidth,
+                source: src,
+                target: dst,
+                strength: config.force.linkStrength.host
+            };
+            network.links.push(link);
+        });
+    }
+
+    function createLayout() {
+
+        var cfg = config.force;
+
+        network.force = d3.layout.force()
+            .size([network.forceWidth, network.forceHeight])
+            .nodes(network.nodes)
+            .links(network.links)
+            .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
+            .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
+            .charge(function(d) { return cfg.charge[d.class]; })
+            .on('tick', tick);
+
+        network.svg = d3.select('#view').append('svg')
+            .attr('width', netView.width)
+            .attr('height', netView.height)
+            .append('g')
+            .attr('transform', config.force.translate());
+//            .attr('id', 'zoomable')
+//            .call(d3.behavior.zoom().on("zoom", zoomRedraw));
+
+        network.svg.append('svg:image')
+            .attr({
+                id: 'bg',
+                width: netView.width,
+                height: netView.height,
+                'xlink:href': config.backgroundUrl
+            })
+            .style('visibility',
+                    config.options.loadBackground ? 'visible' : 'hidden');
+
+//        function zoomRedraw() {
+//            d3.select("#zoomable").attr("transform",
+//                    "translate(" + d3.event.translate + ")"
+//                    + " scale(" + d3.event.scale + ")");
+//        }
+
+        // TODO: move glow/blur stuff to util script
+        var glow = network.svg.append('filter')
+            .attr('x', '-50%')
+            .attr('y', '-50%')
+            .attr('width', '200%')
+            .attr('height', '200%')
+            .attr('id', 'blue-glow');
+
+        glow.append('feColorMatrix')
+            .attr('type', 'matrix')
+            .attr('values', '0 0 0 0  0 ' +
+                '0 0 0 0  0 ' +
+                '0 0 0 0  .7 ' +
+                '0 0 0 1  0 ');
+
+        glow.append('feGaussianBlur')
+            .attr('stdDeviation', 3)
+            .attr('result', 'coloredBlur');
+
+        glow.append('feMerge').selectAll('feMergeNode')
+            .data(['coloredBlur', 'SourceGraphic'])
+            .enter().append('feMergeNode')
+            .attr('in', String);
+
+        // TODO: legend (and auto adjust on scroll)
+//        $('#view').on('scroll', function() {
+//
+//        });
+
+
+        // TODO: move drag behavior into separate method.
+        // == define node drag behavior...
+        network.draggedThreshold = d3.scale.linear()
+            .domain([0, 0.1])
+            .range([5, 20])
+            .clamp(true);
+
+        function dragged(d) {
+            var threshold = network.draggedThreshold(network.force.alpha()),
+                dx = d.oldX - d.px,
+                dy = d.oldY - d.py;
+            if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+                d.dragged = true;
+            }
+            return d.dragged;
+        }
+
+        network.drag = d3.behavior.drag()
+            .origin(function(d) { return d; })
+            .on('dragstart', function(d) {
+                d.oldX = d.x;
+                d.oldY = d.y;
+                d.dragged = false;
+                d.fixed |= 2;
+            })
+            .on('drag', function(d) {
+                d.px = d3.event.x;
+                d.py = d3.event.y;
+                if (dragged(d)) {
+                    if (!network.force.alpha()) {
+                        network.force.alpha(.025);
+                    }
+                }
+            })
+            .on('dragend', function(d) {
+                if (!dragged(d)) {
+                    selectObject(d, this);
+                }
+                d.fixed &= ~6;
+
+                // once we've finished moving, pin the node in position,
+                // if it is a device (not a host)
+                if (d.class === 'device') {
+                    d.fixed = true;
+                    d3.select(this).classed('fixed', true)
+                }
+            });
+
+        $('#view').on('click', function(e) {
+            if (!$(e.target).closest('.node').length) {
+                deselectObject();
+            }
+        });
+
+        // ...............................................................
+
+        // add links to the display
+        network.link = network.svg.append('g').attr('id', 'links')
+            .selectAll('.link')
+            .data(network.force.links(), function(d) {return d.id})
+            .enter().append('line')
+            .attr('class', function(d) {return 'link ' + d.class});
+
+        network.linkSrcPort = network.svg.append('g')
+            .attr({
+                id: 'srcPorts',
+                class: 'portLayer'
+            });
+        network.linkTgtPort = network.svg.append('g')
+            .attr({
+                id: 'tgtPorts',
+                class: 'portLayer'
+            });
+
+        var portVis = portLabelsOn ? 'visible' : 'hidden',
+            pw = config.labels.port.width,
+            ph = config.labels.port.height;
+
+        network.link.filter('.infra').each(function(d) {
+            var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort',
+                tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort';
+
+            if (d.source.type)
+
+            network.linkSrcPort.append('rect').attr({
+                id: 'srcPort-' + safeId(d.id),
+                class: 'port ' + srcType,
+                width: pw,
+                height: ph,
+                rx: 4,
+                ry: 4
+            }).style('visibility', portVis);
+
+            network.linkTgtPort.append('rect').attr({
+                id: 'tgtPort-' + safeId(d.id),
+                class: 'port ' + tgtType,
+                width: pw,
+                height: ph,
+                rx: 4,
+                ry: 4
+            }).style('visibility', portVis);
+
+            network.linkSrcPort.append('text').attr({
+                id: 'srcText-' + safeId(d.id),
+                class: 'portText ' + srcType
+            }).text(d.srcPort)
+                .style('visibility', portVis);
+
+            network.linkTgtPort.append('text').attr({
+                id: 'tgtText-' + safeId(d.id),
+                class: 'portText ' + tgtType
+            }).text(d.tgtPort)
+                .style('visibility', portVis);
+        });
+
+        // ...............................................................
+
+        // add nodes to the display
+        network.node = network.svg.selectAll('.node')
+            .data(network.force.nodes(), function(d) {return d.id})
+            .enter().append('g')
+            .attr('class', function(d) {
+                var cls = 'node ' + d.class;
+                if (d.type) {
+                    cls += ' ' + d.type;
+                }
+                return cls;
+            })
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            })
+            .call(network.drag)
+            .on('mouseover', function(d) {
+                // TODO: show tooltip
+                if (network.mouseoutTimeout) {
+                    clearTimeout(network.mouseoutTimeout);
+                    network.mouseoutTimeout = null;
+                }
+                hoverObject(d);
+            })
+            .on('mouseout', function(d) {
+                // TODO: hide tooltip
+                if (network.mouseoutTimeout) {
+                    clearTimeout(network.mouseoutTimeout);
+                    network.mouseoutTimeout = null;
+                }
+                network.mouseoutTimeout = setTimeout(function() {
+                    hoverObject(null);
+                }, config.mouseOutTimerDelayMs);
+            });
+
+
+        // deal with device nodes first
+        network.nodeRect = network.node.filter('.device')
+            .append('rect')
+            .attr({
+                rx: 5,
+                ry: 5,
+                width: 100,
+                height: 12
+            });
+            // note that width/height are adjusted to fit the label text
+            // then padded, and space made for the icon.
+
+        network.node.filter('.device').each(function(d) {
+            var node = d3.select(this),
+                icon = iconUrl(d);
+
+            node.append('text')
+            // TODO: add label cycle behavior
+                .text(d.id)
+                .attr('dy', '1.1em');
+
+            if (icon) {
+                var cfg = config.icons;
+                node.append('svg:image')
+                    .attr({
+                        width: cfg.w,
+                        height: cfg.h,
+                        'xlink:href': icon
+                    });
+                // note, icon relative positioning (x,y) is done after we have
+                // adjusted the bounds of the rectangle...
+            }
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('rect').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
+
+        // now process host nodes
+        network.nodeCircle = network.node.filter('.host')
+            .append('circle')
+            .attr({
+                r: config.hostRadius
+            });
+
+        network.node.filter('.host').each(function(d) {
+            var node = d3.select(this),
+                icon = iconUrl(d);
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('circle').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
+
+        // this function is scheduled to happen soon after the given thread ends
+        setTimeout(function() {
+            var lab = config.labels,
+                portGap = lab.port.gap,
+                midW = portGap + lab.port.width/ 2,
+                midH = portGap + lab.port.height / 2;
+
+            // post process the device nodes, to pad their size to fit the
+            // label text and attach the icon to the right location.
+            network.node.filter('.device').each(function(d) {
+                // for every node, recompute size, padding, etc. so text fits
+                var node = d3.select(this),
+                    text = node.select('text'),
+                    box = adjustRectToFitText(node);
+
+                // now make the computed adjustment
+                node.select('rect')
+                    .attr(box);
+
+                node.select('image')
+                    .attr('x', box.x + config.icons.xoff)
+                    .attr('y', box.y + config.icons.yoff);
+
+                var bounds = boundsFromBox(box),
+                    portBounds = {
+                        x1: bounds.x1 - midW,
+                        x2: bounds.x2 + midW,
+                        y1: bounds.y1 - midH,
+                        y2: bounds.y2 + midH
+                    };
+
+                // todo: clean up extent and edge work..
+                d.extent = {
+                    left: bounds.x1 - lab.marginLR,
+                    right: bounds.x2 + lab.marginLR,
+                    top: bounds.y1 - lab.marginTB,
+                    bottom: bounds.y2 + lab.marginTB
+                };
+
+                d.edge = {
+                    left   : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
+                    right  : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
+                    top    : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
+                    bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
+                };
+
+                d.portEdge = {
+                    left   : new geo.LineSegment(
+                        portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2
+                    ),
+                    right  : new geo.LineSegment(
+                        portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2
+                    ),
+                    top    : new geo.LineSegment(
+                        portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1
+                    ),
+                    bottom : new geo.LineSegment(
+                        portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2
+                    )
+                };
+
+            });
+
+            network.numTicks = 0;
+            network.preventCollisions = false;
+            network.force.start();
+            for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
+                network.force.tick();
+            }
+            network.preventCollisions = true;
+            $('#view').css('visibility', 'visible');
+        });
+
+
+        // returns the newly computed bounding box of the rectangle
+        function adjustRectToFitText(n) {
+            var text = n.select('text'),
+                box = text.node().getBBox(),
+                lab = config.labels;
+
+            // not sure why n.data() returns an array of 1 element...
+            var data = n.data()[0];
+
+            text.attr('text-anchor', 'middle')
+                .attr('y', '-0.8em')
+                .attr('x', lab.imgPad/2)
+            ;
+
+            // translate the bbox so that it is centered on [x,y]
+            box.x = -box.width / 2;
+            box.y = -box.height / 2;
+
+            // add padding
+            box.x -= (lab.padLR + lab.imgPad/2);
+            box.width += lab.padLR * 2 + lab.imgPad;
+            box.y -= lab.padTB;
+            box.height += lab.padTB * 2;
+
+            return box;
+        }
+
+        function boundsFromBox(box) {
+            return {
+                x1: box.x,
+                y1: box.y,
+                x2: box.x + box.width,
+                y2: box.y + box.height
+            };
+        }
+
+    }
+
+    function iconUrl(d) {
+        return 'img/' + d.type + '.png';
+//        return config.iconUrl[d.icon];
+    }
+
+    function translate(x, y) {
+        return 'translate(' + x + ',' + y + ')';
+    }
+
+    // prevents collisions amongst device nodes
+    function preventCollisions() {
+        var quadtree = d3.geom.quadtree(network.nodes),
+            hrad = config.hostRadius;
+
+        network.nodes.forEach(function(n) {
+            var nx1, nx2, ny1, ny2;
+
+            if (n.class === 'device') {
+                nx1 = n.x + n.extent.left;
+                nx2 = n.x + n.extent.right;
+                ny1 = n.y + n.extent.top;
+                ny2 = n.y + n.extent.bottom;
+
+            } else {
+                nx1 = n.x - hrad;
+                nx2 = n.x + hrad;
+                ny1 = n.y - hrad;
+                ny2 = n.y + hrad;
+            }
+
+            quadtree.visit(function(quad, x1, y1, x2, y2) {
+                if (quad.point && quad.point !== n) {
+                    // check if the rectangles/circles intersect
+                    var p = quad.point,
+                        px1, px2, py1, py2, ix;
+
+                    if (p.class === 'device') {
+                        px1 = p.x + p.extent.left;
+                        px2 = p.x + p.extent.right;
+                        py1 = p.y + p.extent.top;
+                        py2 = p.y + p.extent.bottom;
+
+                    } else {
+                        px1 = p.x - hrad;
+                        px2 = p.x + hrad;
+                        py1 = p.y - hrad;
+                        py2 = p.y + hrad;
+                    }
+
+                    ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
+
+                    if (ix) {
+                        var xa1 = nx2 - px1, // shift n left , p right
+                            xa2 = px2 - nx1, // shift n right, p left
+                            ya1 = ny2 - py1, // shift n up   , p down
+                            ya2 = py2 - ny1, // shift n down , p up
+                            adj = Math.min(xa1, xa2, ya1, ya2);
+
+                        if (adj == xa1) {
+                            n.x -= adj / 2;
+                            p.x += adj / 2;
+                        } else if (adj == xa2) {
+                            n.x += adj / 2;
+                            p.x -= adj / 2;
+                        } else if (adj == ya1) {
+                            n.y -= adj / 2;
+                            p.y += adj / 2;
+                        } else if (adj == ya2) {
+                            n.y += adj / 2;
+                            p.y -= adj / 2;
+                        }
+                    }
+                    return ix;
+                }
+            });
+
+        });
+    }
+
+    function tick(e) {
+        network.numTicks++;
+
+        if (config.options.layering) {
+            // adjust the y-coord of each node, based on y-pos constraints
+            network.nodes.forEach(function (n) {
+                var z = e.alpha * n.constraint.weight;
+                if (!isNaN(n.constraint.y)) {
+                    n.y = (n.constraint.y * z + n.y * (1 - z));
+                }
+            });
+        }
+
+        if (config.options.collisionPrevention && network.preventCollisions) {
+            preventCollisions();
+        }
+
+        var portHalfW = config.labels.port.width / 2,
+            portHalfH = config.labels.port.height / 2;
+
+        // clip visualization of links at bounds of nodes...
+        network.link.each(function(d) {
+            var xs = d.source.x,
+                ys = d.source.y,
+                xt = d.target.x,
+                yt = d.target.y,
+                line = new geo.LineSegment(xs, ys, xt, yt),
+                e, ix,
+                exs, eys, ext, eyt,
+                pxs, pys, pxt, pyt;
+
+            if (d.class === 'host') {
+                // no adjustment for source end of link, since hosts are dots
+                exs = xs;
+                eys = ys;
+
+            } else {
+                for (e in d.source.edge) {
+                    ix = line.intersect(d.source.edge[e].offset(xs, ys));
+                    if (ix.in1 && ix.in2) {
+                        exs = ix.x;
+                        eys = ix.y;
+
+                        // also pick off the port label intersection
+                        ix = line.intersect(d.source.portEdge[e].offset(xs, ys));
+                        pxs = ix.x;
+                        pys = ix.y;
+                        break;
+                    }
+                }
+            }
+
+            for (e in d.target.edge) {
+                ix = line.intersect(d.target.edge[e].offset(xt, yt));
+                if (ix.in1 && ix.in2) {
+                    ext = ix.x;
+                    eyt = ix.y;
+
+                    // also pick off the port label intersection
+                    ix = line.intersect(d.target.portEdge[e].offset(xt, yt));
+                    pxt = ix.x;
+                    pyt = ix.y;
+                    break;
+                }
+            }
+
+            // adjust the endpoints of the link's line to match rectangles
+            var sid = safeId(d.id);
+            d3.select(this)
+                .attr('x1', exs)
+                .attr('y1', eys)
+                .attr('x2', ext)
+                .attr('y2', eyt);
+
+            d3.select('#srcPort-' + sid)
+                .attr('x', pxs - portHalfW)
+                .attr('y', pys - portHalfH);
+
+            d3.select('#tgtPort-' + sid)
+                .attr('x', pxt - portHalfW)
+                .attr('y', pyt - portHalfH);
+
+            // TODO: fit label rect to size of port number.
+            d3.select('#srcText-' + sid)
+                .attr('x', pxs - 5)
+                .attr('y', pys + 3);
+
+            d3.select('#tgtText-' + sid)
+                .attr('x', pxt - 5)
+                .attr('y', pyt + 3);
+
+        });
+
+        // position each node by translating the node (group) by x,y
+        network.node
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            });
+
+    }
+
+    //    $('#docs-close').on('click', function() {
+    //        deselectObject();
+    //        return false;
+    //    });
+
+    //    $(document).on('click', '.select-object', function() {
+    //        var obj = graph.data[$(this).data('name')];
+    //        if (obj) {
+    //            selectObject(obj);
+    //        }
+    //        return false;
+    //    });
+
+    function findNodeFromData(d) {
+        var el = null;
+        network.node.filter('.' + d.class).each(function(n) {
+            if (n.id === d.id) {
+                el = d3.select(this);
+            }
+        });
+        return el;
+    }
+
+    function selectObject(obj, el) {
+        var node;
+        if (el) {
+            node = d3.select(el);
+        } else {
+            network.node.each(function(d) {
+                if (d == obj) {
+                    node = d3.select(el = this);
+                }
+            });
+        }
+        if (!node) return;
+
+        if (node.classed('selected')) {
+            deselectObject();
+            flyinPane(null);
+            return;
+        }
+        deselectObject(false);
+
+        selected = {
+            obj : obj,
+            el  : el
+        };
+
+        node.classed('selected', true);
+        flyinPane(obj);
+    }
+
+    function deselectObject(doResize) {
+        // Review: logic of 'resize(...)' function.
+        if (doResize || typeof doResize == 'undefined') {
+            resize(false);
+        }
+
+        // deselect all nodes in the network...
+        network.node.classed('selected', false);
+        selected = {};
+        flyinPane(null);
+    }
+
+    function flyinPane(obj) {
+        var pane = d3.select('#flyout'),
+            url;
+
+        if (obj) {
+            // go get details of the selected object from the server...
+            url = detailJsonUrl(obj.id);
+            d3.json(url, function (err, data) {
+                if (err) {
+                    alert('Oops! Error reading JSON...\n\n' +
+                        'URL: ' + url + '\n\n' +
+                        'Error: ' + err.message);
+                    return;
+                }
+//                console.log("JSON data... " + url);
+//                console.log(data);
+
+                displayDetails(data, pane);
+            });
+
+        } else {
+            // hide pane
+            pane.transition().duration(750)
+                .style('right', '-320px')
+                .style('opacity', 0.0);
+        }
+    }
+
+    function displayDetails(data, pane) {
+        $('#flyout').empty();
+
+        var title = pane.append("h2"),
+            table = pane.append("table"),
+            tbody = table.append("tbody");
+
+        $('<img src="img/' + data.type + '.png">').appendTo(title);
+        $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
+
+
+        // TODO: consider using d3 data bind to TR/TD
+
+        data.propOrder.forEach(function(p) {
+            if (p === '-') {
+                addSep(tbody);
+            } else {
+                addProp(tbody, p, data.props[p]);
+            }
+        });
+
+        function addSep(tbody) {
+            var tr = tbody.append('tr');
+            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
+        }
+
+        function addProp(tbody, label, value) {
+            var tr = tbody.append('tr');
+
+            tr.append('td')
+                .attr('class', 'label')
+                .text(label + ' :');
+
+            tr.append('td')
+                .attr('class', 'value')
+                .text(value);
+        }
+
+        // show pane
+        pane.transition().duration(750)
+            .style('right', '20px')
+            .style('opacity', 1.0);
+    }
+
+    function highlightObject(obj) {
+        if (obj) {
+            if (obj != highlighted) {
+                // TODO set or clear "inactive" class on nodes, based on criteria
+                network.node.classed('inactive', function(d) {
+                    //                return (obj !== d &&
+                    //                    d.relation(obj.id));
+                    return (obj !== d);
+                });
+                // TODO: same with links
+                network.link.classed('inactive', function(d) {
+                    return (obj !== d.source && obj !== d.target);
+                });
+            }
+            highlighted = obj;
+        } else {
+            if (highlighted) {
+                // clear the inactive flag (no longer suppressed visually)
+                network.node.classed('inactive', false);
+                network.link.classed('inactive', false);
+            }
+            highlighted = null;
+
+        }
+    }
+
+    function hoverObject(obj) {
+        if (obj) {
+            hovered = obj;
+        } else {
+            if (hovered) {
+                hovered = null;
+            }
+        }
+    }
+
+
+    function resize() {
+        netView.height = window.innerHeight - config.mastHeight;
+        netView.width = window.innerWidth;
+        $('#view')
+            .css('height', netView.height + 'px')
+            .css('width', netView.width + 'px');
+
+        network.forceWidth = netView.width - config.force.marginLR;
+        network.forceHeight = netView.height - config.force.marginTB;
+    }
+
+    // ======================================================================
+    // register with the UI framework
+
+    onos.ui.addView('topo', {
+        load: loadNetworkView
+    });
+
+
+}(ONOS));
+
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 2e058c1..88fcd94 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -20,3 +20,7 @@
  @author Simon Hunt
  */
 
+svg #topo-bg {
+    opacity: 0.5;
+}
+
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index 8d1f9c1..fcc4bb4 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -15,7 +15,7 @@
  */
 
 /*
- ONOS network topology viewer - PoC version 1.0
+ ONOS network topology viewer - version 1.1
 
  @author Simon Hunt
  */
@@ -25,7 +25,7 @@
 
     // configuration data
     var config = {
-        useLiveData: true,
+        useLiveData: false,
         debugOn: false,
         debug: {
             showNodeXY: false,
@@ -34,7 +34,7 @@
         options: {
             layering: true,
             collisionPrevention: true,
-            loadBackground: true
+            showBackground: true
         },
         backgroundUrl: 'img/us-map.png',
         data: {
@@ -55,22 +55,7 @@
             pkt: 'img/pkt.png',
             opt: 'img/opt.png'
         },
-        mastHeight: 36,
         force: {
-            note: 'node.class or link.class is used to differentiate',
-            linkDistance: {
-                infra: 200,
-                host: 40
-            },
-            linkStrength: {
-                infra: 1.0,
-                host: 1.0
-            },
-            charge: {
-                device: -800,
-                host: -1000
-            },
-            ticksWithoutCollisions: 50,
             marginLR: 20,
             marginTB: 20,
             translate: function() {
@@ -78,39 +63,19 @@
                     config.force.marginLR + ',' +
                     config.force.marginTB + ')';
             }
-        },
-        labels: {
-            imgPad: 16,
-            padLR: 8,
-            padTB: 6,
-            marginLR: 3,
-            marginTB: 2,
-            port: {
-                gap: 3,
-                width: 18,
-                height: 14
-            }
-        },
-        icons: {
-            w: 32,
-            h: 32,
-            xoff: -12,
-            yoff: -8
-        },
-        constraints: {
-            ypos: {
-                host: 0.05,
-                switch: 0.3,
-                roadm: 0.7
-            }
-        },
-        hostLinkWidth: 1.0,
-        hostRadius: 7,
-        mouseOutTimerDelayMs: 120
+        }
     };
 
+    // radio buttons
+    var btnSet = [
+            { id: 'showAll', text: 'All Layers' },
+            { id: 'showPkt', text: 'Packet Only' },
+            { id: 'showOpt', text: 'Optical Only' }
+        ];
+
     // state variables
-    var view = {},
+    var svg,
+        bgImg,
         network = {},
         selected = {},
         highlighted = null,
@@ -119,84 +84,19 @@
         portLabelsOn = false;
 
 
-    function debug(what) {
-        return config.debugOn && config.debug[what];
-    }
+    // ==============================
+    // Private functions
 
-    function urlData() {
-        return config.data[config.useLiveData ? 'live' : 'fake'];
-    }
-
-    function networkJsonUrl() {
-        return urlData().jsonUrl;
-    }
-
-    function safeId(id) {
-        return id.replace(/[^a-z0-9]/gi, '_');
-    }
-
-    function detailJsonUrl(id) {
-        var u = urlData(),
-            encId = config.useLiveData ? encodeURIComponent(id) : safeId(id);
-        return u.detailPrefix + encId + u.detailSuffix;
-    }
-
-
-    // load the topology view of the network
-    function loadNetworkView() {
-        // Hey, here I am, calling something on the ONOS api:
-        api.printTime();
-
-        resize();
-
-        // go get our network data from the server...
-        var url = networkJsonUrl();
-        d3.json(url , function (err, data) {
-            if (err) {
-                alert('Oops! Error reading JSON...\n\n' +
-                    'URL: ' + url + '\n\n' +
-                    'Error: ' + err.message);
-                return;
-            }
-//            console.log("here is the JSON data...");
-//            console.log(data);
-
-            network.data = data;
-            drawNetwork();
-        });
-
-        // while we wait for the data, set up the handlers...
-        setUpClickHandler();
-        setUpRadioButtonHandler();
-        setUpKeyHandler();
-        $(window).on('resize', resize);
-    }
-
-    function setUpClickHandler() {
-        // click handler for "selectable" objects
-        $(document).on('click', '.select-object', function () {
-            // when any object of class "select-object" is clicked...
-            var obj = network.lookup[$(this).data('id')];
-            if (obj) {
-                selectObject(obj);
-            }
-            // stop propagation of event (I think) ...
-            return false;
+    // set the size of the SVG layer (or other element) to that of the view
+    function setSize(view, el) {
+        var thing = el || svg;
+        thing.attr({
+            width: view.width(),
+            height: view.height()
         });
     }
 
-    function setUpRadioButtonHandler() {
-        d3.selectAll('#displayModes .radio').on('click', function () {
-            var id = d3.select(this).attr('id');
-            if (id !== viewMode) {
-                radioButton('displayModes', id);
-                viewMode = id;
-                doRadioAction(id);
-            }
-        });
-    }
-
-    function doRadioAction(id) {
+    function doRadio(view, id) {
         showAllLayers();
         if (id === 'showPkt') {
             showPacketLayer();
@@ -206,1014 +106,68 @@
     }
 
     function showAllLayers() {
-        network.node.classed('inactive', false);
-        network.link.classed('inactive', false);
-        d3.selectAll('svg .port').classed('inactive', false)
-        d3.selectAll('svg .portText').classed('inactive', false)
+//        network.node.classed('inactive', false);
+//        network.link.classed('inactive', false);
+//        d3.selectAll('svg .port').classed('inactive', false);
+//        d3.selectAll('svg .portText').classed('inactive', false);
+        alert('show all layers');
     }
 
     function showPacketLayer() {
-        network.node.each(function(d) {
-            // deactivate nodes that are not hosts or switches
-            if (d.class === 'device' && d.type !== 'switch') {
-                d3.select(this).classed('inactive', true);
-            }
-        });
-
-        network.link.each(function(lnk) {
-            // deactivate infrastructure links that have opt's as endpoints
-            if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
-                d3.select(this).classed('inactive', true);
-            }
-        });
-
-        // deactivate non-packet ports
-        d3.selectAll('svg .optPort').classed('inactive', true)
+        alert('show packet layer');
     }
 
     function showOpticalLayer() {
-        network.node.each(function(d) {
-            // deactivate nodes that are not optical devices
-            if (d.type !== 'roadm') {
-                d3.select(this).classed('inactive', true);
-            }
-        });
-
-        network.link.each(function(lnk) {
-            // deactivate infrastructure links that have opt's as endpoints
-            if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
-                d3.select(this).classed('inactive', true);
-            }
-        });
-
-        // deactivate non-packet ports
-        d3.selectAll('svg .pktPort').classed('inactive', true)
+        alert('show optical layer');
     }
 
-    function setUpKeyHandler() {
-        d3.select('body')
-            .on('keydown', function () {
-                processKeyEvent();
-                if (debug('showKeyHandler')) {
-                    network.svg.append('text')
-                        .attr('x', 5)
-                        .attr('y', 15)
-                        .style('font-size', '20pt')
-                        .text('keyCode: ' + d3.event.keyCode +
-                            ' applied to : ' + contextLabel())
-                        .transition().duration(2000)
-                        .style('font-size', '2pt')
-                        .style('fill-opacity', 0.01)
-                        .remove();
-                }
-            });
-    }
+    // ==============================
+    // View life-cycle callbacks
 
-    function contextLabel() {
-        return hovered === null ? "(nothing)" : hovered.id;
-    }
+    function preload(view, ctx) {
+        var w = view.width(),
+            h = view.height(),
+            idBg = view.uid('bg'),
+            showBg = config.options.showBackground ? 'visible' : 'hidden';
 
-    function radioButton(group, id) {
-        d3.selectAll("#" + group + " .radio").classed("active", false);
-        d3.select("#" + group + " #" + id).classed("active", true);
-    }
-
-    function processKeyEvent() {
-        var code = d3.event.keyCode;
-        switch (code) {
-            case 66:    // B
-                toggleBackground();
-                break;
-            case 71:    // G
-                cycleLayout();
-                break;
-            case 76:    // L
-                cycleLabels();
-                break;
-            case 80:    // P
-                togglePorts();
-                break;
-            case 85:    // U
-                unpin();
-                break;
-        }
-
-    }
-
-    function toggleBackground() {
-        var bg = d3.select('#bg'),
-            vis = bg.style('visibility'),
-            newvis = (vis === 'hidden') ? 'visible' : 'hidden';
-        bg.style('visibility', newvis);
-    }
-
-    function cycleLayout() {
-        config.options.layering = !config.options.layering;
-        network.force.resume();
-    }
-
-    function cycleLabels() {
-        console.log('Cycle Labels - context = ' + contextLabel());
-    }
-
-    function togglePorts() {
-        portLabelsOn = !portLabelsOn;
-        var portVis = portLabelsOn ? 'visible' : 'hidden';
-        d3.selectAll('.port').style('visibility', portVis);
-        d3.selectAll('.portText').style('visibility', portVis);
-    }
-
-    function unpin() {
-        if (hovered) {
-            hovered.fixed = false;
-            findNodeFromData(hovered).classed('fixed', false);
-            network.force.resume();
-        }
-        console.log('Unpin - context = ' + contextLabel());
-    }
-
-
-    // ========================================================
-
-    function drawNetwork() {
-        $('#view').empty();
-
-        prepareNodesAndLinks();
-        createLayout();
-        console.log("\n\nHere is the augmented network object...");
-        console.log(network);
-    }
-
-    function prepareNodesAndLinks() {
-        network.lookup = {};
-        network.nodes = [];
-        network.links = [];
-
-        var nw = network.forceWidth,
-            nh = network.forceHeight;
-
-        function yPosConstraintForNode(n) {
-            return config.constraints.ypos[n.type || 'host'];
-        }
-
-        // Note that both 'devices' and 'hosts' get mapped into the nodes array
-
-        // first, the devices...
-        network.data.devices.forEach(function(n) {
-            var ypc = yPosConstraintForNode(n),
-                ix = Math.random() * 0.6 * nw + 0.2 * nw,
-                iy = ypc * nh,
-                node = {
-                    id: n.id,
-                    labels: n.labels,
-                    class: 'device',
-                    icon: 'device',
-                    type: n.type,
-                    x: ix,
-                    y: iy,
-                    constraint: {
-                        weight: 0.7,
-                        y: iy
-                    }
-                };
-            network.lookup[n.id] = node;
-            network.nodes.push(node);
-        });
-
-        // then, the hosts...
-        network.data.hosts.forEach(function(n) {
-            var ypc = yPosConstraintForNode(n),
-                ix = Math.random() * 0.6 * nw + 0.2 * nw,
-                iy = ypc * nh,
-                node = {
-                    id: n.id,
-                    labels: n.labels,
-                    class: 'host',
-                    icon: 'host',
-                    type: n.type,
-                    x: ix,
-                    y: iy,
-                    constraint: {
-                        weight: 0.7,
-                        y: iy
-                    }
-                };
-            network.lookup[n.id] = node;
-            network.nodes.push(node);
-        });
-
-
-        // now, process the explicit links...
-        network.data.links.forEach(function(lnk) {
-            var src = network.lookup[lnk.src],
-                dst = network.lookup[lnk.dst],
-                id = src.id + "-" + dst.id;
-
-            var link = {
-                class: 'infra',
-                id: id,
-                type: lnk.type,
-                width: lnk.linkWidth,
-                source: src,
-                srcPort: lnk.srcPort,
-                target: dst,
-                tgtPort: lnk.dstPort,
-                strength: config.force.linkStrength.infra
-            };
-            network.links.push(link);
-        });
-
-        // finally, infer host links...
-        network.data.hosts.forEach(function(n) {
-            var src = network.lookup[n.id],
-                dst = network.lookup[n.cp.device],
-                id = src.id + "-" + dst.id;
-
-            var link = {
-                class: 'host',
-                id: id,
-                type: 'hostLink',
-                width: config.hostLinkWidth,
-                source: src,
-                target: dst,
-                strength: config.force.linkStrength.host
-            };
-            network.links.push(link);
-        });
-    }
-
-    function createLayout() {
-
-        var cfg = config.force;
-
-        network.force = d3.layout.force()
-            .size([network.forceWidth, network.forceHeight])
-            .nodes(network.nodes)
-            .links(network.links)
-            .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
-            .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
-            .charge(function(d) { return cfg.charge[d.class]; })
-            .on('tick', tick);
-
-        network.svg = d3.select('#view').append('svg')
-            .attr('width', view.width)
-            .attr('height', view.height)
-            .append('g')
+        // NOTE: view.$div is a D3 selection of the view's div
+        svg = view.$div.append('svg');
+        setSize(view);
+        svg.append('g')
             .attr('transform', config.force.translate());
-//            .attr('id', 'zoomable')
-//            .call(d3.behavior.zoom().on("zoom", zoomRedraw));
 
-        network.svg.append('svg:image')
+        // load the background image
+        bgImg = svg.append('svg:image')
             .attr({
-                id: 'bg',
-                width: view.width,
-                height: view.height,
+                id: idBg,
+                width: w,
+                height: h,
                 'xlink:href': config.backgroundUrl
             })
-            .style('visibility',
-                    config.options.loadBackground ? 'visible' : 'hidden');
-
-//        function zoomRedraw() {
-//            d3.select("#zoomable").attr("transform",
-//                    "translate(" + d3.event.translate + ")"
-//                    + " scale(" + d3.event.scale + ")");
-//        }
-
-        // TODO: move glow/blur stuff to util script
-        var glow = network.svg.append('filter')
-            .attr('x', '-50%')
-            .attr('y', '-50%')
-            .attr('width', '200%')
-            .attr('height', '200%')
-            .attr('id', 'blue-glow');
-
-        glow.append('feColorMatrix')
-            .attr('type', 'matrix')
-            .attr('values', '0 0 0 0  0 ' +
-                '0 0 0 0  0 ' +
-                '0 0 0 0  .7 ' +
-                '0 0 0 1  0 ');
-
-        glow.append('feGaussianBlur')
-            .attr('stdDeviation', 3)
-            .attr('result', 'coloredBlur');
-
-        glow.append('feMerge').selectAll('feMergeNode')
-            .data(['coloredBlur', 'SourceGraphic'])
-            .enter().append('feMergeNode')
-            .attr('in', String);
-
-        // TODO: legend (and auto adjust on scroll)
-//        $('#view').on('scroll', function() {
-//
-//        });
-
-
-        // TODO: move drag behavior into separate method.
-        // == define node drag behavior...
-        network.draggedThreshold = d3.scale.linear()
-            .domain([0, 0.1])
-            .range([5, 20])
-            .clamp(true);
-
-        function dragged(d) {
-            var threshold = network.draggedThreshold(network.force.alpha()),
-                dx = d.oldX - d.px,
-                dy = d.oldY - d.py;
-            if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
-                d.dragged = true;
-            }
-            return d.dragged;
-        }
-
-        network.drag = d3.behavior.drag()
-            .origin(function(d) { return d; })
-            .on('dragstart', function(d) {
-                d.oldX = d.x;
-                d.oldY = d.y;
-                d.dragged = false;
-                d.fixed |= 2;
-            })
-            .on('drag', function(d) {
-                d.px = d3.event.x;
-                d.py = d3.event.y;
-                if (dragged(d)) {
-                    if (!network.force.alpha()) {
-                        network.force.alpha(.025);
-                    }
-                }
-            })
-            .on('dragend', function(d) {
-                if (!dragged(d)) {
-                    selectObject(d, this);
-                }
-                d.fixed &= ~6;
-
-                // once we've finished moving, pin the node in position,
-                // if it is a device (not a host)
-                if (d.class === 'device') {
-                    d.fixed = true;
-                    d3.select(this).classed('fixed', true)
-                }
+            .style({
+                visibility: showBg
             });
-
-        $('#view').on('click', function(e) {
-            if (!$(e.target).closest('.node').length) {
-                deselectObject();
-            }
-        });
-
-        // ...............................................................
-
-        // add links to the display
-        network.link = network.svg.append('g').attr('id', 'links')
-            .selectAll('.link')
-            .data(network.force.links(), function(d) {return d.id})
-            .enter().append('line')
-            .attr('class', function(d) {return 'link ' + d.class});
-
-        network.linkSrcPort = network.svg.append('g')
-            .attr({
-                id: 'srcPorts',
-                class: 'portLayer'
-            });
-        network.linkTgtPort = network.svg.append('g')
-            .attr({
-                id: 'tgtPorts',
-                class: 'portLayer'
-            });
-
-        var portVis = portLabelsOn ? 'visible' : 'hidden',
-            pw = config.labels.port.width,
-            ph = config.labels.port.height;
-
-        network.link.filter('.infra').each(function(d) {
-            var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort',
-                tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort';
-
-            if (d.source.type)
-
-            network.linkSrcPort.append('rect').attr({
-                id: 'srcPort-' + safeId(d.id),
-                class: 'port ' + srcType,
-                width: pw,
-                height: ph,
-                rx: 4,
-                ry: 4
-            }).style('visibility', portVis);
-
-            network.linkTgtPort.append('rect').attr({
-                id: 'tgtPort-' + safeId(d.id),
-                class: 'port ' + tgtType,
-                width: pw,
-                height: ph,
-                rx: 4,
-                ry: 4
-            }).style('visibility', portVis);
-
-            network.linkSrcPort.append('text').attr({
-                id: 'srcText-' + safeId(d.id),
-                class: 'portText ' + srcType
-            }).text(d.srcPort)
-                .style('visibility', portVis);
-
-            network.linkTgtPort.append('text').attr({
-                id: 'tgtText-' + safeId(d.id),
-                class: 'portText ' + tgtType
-            }).text(d.tgtPort)
-                .style('visibility', portVis);
-        });
-
-        // ...............................................................
-
-        // add nodes to the display
-        network.node = network.svg.selectAll('.node')
-            .data(network.force.nodes(), function(d) {return d.id})
-            .enter().append('g')
-            .attr('class', function(d) {
-                var cls = 'node ' + d.class;
-                if (d.type) {
-                    cls += ' ' + d.type;
-                }
-                return cls;
-            })
-            .attr('transform', function(d) {
-                return translate(d.x, d.y);
-            })
-            .call(network.drag)
-            .on('mouseover', function(d) {
-                // TODO: show tooltip
-                if (network.mouseoutTimeout) {
-                    clearTimeout(network.mouseoutTimeout);
-                    network.mouseoutTimeout = null;
-                }
-                hoverObject(d);
-            })
-            .on('mouseout', function(d) {
-                // TODO: hide tooltip
-                if (network.mouseoutTimeout) {
-                    clearTimeout(network.mouseoutTimeout);
-                    network.mouseoutTimeout = null;
-                }
-                network.mouseoutTimeout = setTimeout(function() {
-                    hoverObject(null);
-                }, config.mouseOutTimerDelayMs);
-            });
-
-
-        // deal with device nodes first
-        network.nodeRect = network.node.filter('.device')
-            .append('rect')
-            .attr({
-                rx: 5,
-                ry: 5,
-                width: 100,
-                height: 12
-            });
-            // note that width/height are adjusted to fit the label text
-            // then padded, and space made for the icon.
-
-        network.node.filter('.device').each(function(d) {
-            var node = d3.select(this),
-                icon = iconUrl(d);
-
-            node.append('text')
-            // TODO: add label cycle behavior
-                .text(d.id)
-                .attr('dy', '1.1em');
-
-            if (icon) {
-                var cfg = config.icons;
-                node.append('svg:image')
-                    .attr({
-                        width: cfg.w,
-                        height: cfg.h,
-                        'xlink:href': icon
-                    });
-                // note, icon relative positioning (x,y) is done after we have
-                // adjusted the bounds of the rectangle...
-            }
-
-            // debug function to show the modelled x,y coordinates of nodes...
-            if (debug('showNodeXY')) {
-                node.select('rect').attr('fill-opacity', 0.5);
-                node.append('circle')
-                    .attr({
-                        class: 'debug',
-                        cx: 0,
-                        cy: 0,
-                        r: '3px'
-                    });
-            }
-        });
-
-        // now process host nodes
-        network.nodeCircle = network.node.filter('.host')
-            .append('circle')
-            .attr({
-                r: config.hostRadius
-            });
-
-        network.node.filter('.host').each(function(d) {
-            var node = d3.select(this),
-                icon = iconUrl(d);
-
-            // debug function to show the modelled x,y coordinates of nodes...
-            if (debug('showNodeXY')) {
-                node.select('circle').attr('fill-opacity', 0.5);
-                node.append('circle')
-                    .attr({
-                        class: 'debug',
-                        cx: 0,
-                        cy: 0,
-                        r: '3px'
-                    });
-            }
-        });
-
-        // this function is scheduled to happen soon after the given thread ends
-        setTimeout(function() {
-            var lab = config.labels,
-                portGap = lab.port.gap,
-                midW = portGap + lab.port.width/ 2,
-                midH = portGap + lab.port.height / 2;
-
-            // post process the device nodes, to pad their size to fit the
-            // label text and attach the icon to the right location.
-            network.node.filter('.device').each(function(d) {
-                // for every node, recompute size, padding, etc. so text fits
-                var node = d3.select(this),
-                    text = node.select('text'),
-                    box = adjustRectToFitText(node);
-
-                // now make the computed adjustment
-                node.select('rect')
-                    .attr(box);
-
-                node.select('image')
-                    .attr('x', box.x + config.icons.xoff)
-                    .attr('y', box.y + config.icons.yoff);
-
-                var bounds = boundsFromBox(box),
-                    portBounds = {
-                        x1: bounds.x1 - midW,
-                        x2: bounds.x2 + midW,
-                        y1: bounds.y1 - midH,
-                        y2: bounds.y2 + midH
-                    };
-
-                // todo: clean up extent and edge work..
-                d.extent = {
-                    left: bounds.x1 - lab.marginLR,
-                    right: bounds.x2 + lab.marginLR,
-                    top: bounds.y1 - lab.marginTB,
-                    bottom: bounds.y2 + lab.marginTB
-                };
-
-                d.edge = {
-                    left   : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
-                    right  : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
-                    top    : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
-                    bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
-                };
-
-                d.portEdge = {
-                    left   : new geo.LineSegment(
-                        portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2
-                    ),
-                    right  : new geo.LineSegment(
-                        portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2
-                    ),
-                    top    : new geo.LineSegment(
-                        portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1
-                    ),
-                    bottom : new geo.LineSegment(
-                        portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2
-                    )
-                };
-
-            });
-
-            network.numTicks = 0;
-            network.preventCollisions = false;
-            network.force.start();
-            for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
-                network.force.tick();
-            }
-            network.preventCollisions = true;
-            $('#view').css('visibility', 'visible');
-        });
-
-
-        // returns the newly computed bounding box of the rectangle
-        function adjustRectToFitText(n) {
-            var text = n.select('text'),
-                box = text.node().getBBox(),
-                lab = config.labels;
-
-            // not sure why n.data() returns an array of 1 element...
-            var data = n.data()[0];
-
-            text.attr('text-anchor', 'middle')
-                .attr('y', '-0.8em')
-                .attr('x', lab.imgPad/2)
-            ;
-
-            // translate the bbox so that it is centered on [x,y]
-            box.x = -box.width / 2;
-            box.y = -box.height / 2;
-
-            // add padding
-            box.x -= (lab.padLR + lab.imgPad/2);
-            box.width += lab.padLR * 2 + lab.imgPad;
-            box.y -= lab.padTB;
-            box.height += lab.padTB * 2;
-
-            return box;
-        }
-
-        function boundsFromBox(box) {
-            return {
-                x1: box.x,
-                y1: box.y,
-                x2: box.x + box.width,
-                y2: box.y + box.height
-            };
-        }
-
-    }
-
-    function iconUrl(d) {
-        return 'img/' + d.type + '.png';
-//        return config.iconUrl[d.icon];
-    }
-
-    function translate(x, y) {
-        return 'translate(' + x + ',' + y + ')';
-    }
-
-    // prevents collisions amongst device nodes
-    function preventCollisions() {
-        var quadtree = d3.geom.quadtree(network.nodes),
-            hrad = config.hostRadius;
-
-        network.nodes.forEach(function(n) {
-            var nx1, nx2, ny1, ny2;
-
-            if (n.class === 'device') {
-                nx1 = n.x + n.extent.left;
-                nx2 = n.x + n.extent.right;
-                ny1 = n.y + n.extent.top;
-                ny2 = n.y + n.extent.bottom;
-
-            } else {
-                nx1 = n.x - hrad;
-                nx2 = n.x + hrad;
-                ny1 = n.y - hrad;
-                ny2 = n.y + hrad;
-            }
-
-            quadtree.visit(function(quad, x1, y1, x2, y2) {
-                if (quad.point && quad.point !== n) {
-                    // check if the rectangles/circles intersect
-                    var p = quad.point,
-                        px1, px2, py1, py2, ix;
-
-                    if (p.class === 'device') {
-                        px1 = p.x + p.extent.left;
-                        px2 = p.x + p.extent.right;
-                        py1 = p.y + p.extent.top;
-                        py2 = p.y + p.extent.bottom;
-
-                    } else {
-                        px1 = p.x - hrad;
-                        px2 = p.x + hrad;
-                        py1 = p.y - hrad;
-                        py2 = p.y + hrad;
-                    }
-
-                    ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
-
-                    if (ix) {
-                        var xa1 = nx2 - px1, // shift n left , p right
-                            xa2 = px2 - nx1, // shift n right, p left
-                            ya1 = ny2 - py1, // shift n up   , p down
-                            ya2 = py2 - ny1, // shift n down , p up
-                            adj = Math.min(xa1, xa2, ya1, ya2);
-
-                        if (adj == xa1) {
-                            n.x -= adj / 2;
-                            p.x += adj / 2;
-                        } else if (adj == xa2) {
-                            n.x += adj / 2;
-                            p.x -= adj / 2;
-                        } else if (adj == ya1) {
-                            n.y -= adj / 2;
-                            p.y += adj / 2;
-                        } else if (adj == ya2) {
-                            n.y += adj / 2;
-                            p.y -= adj / 2;
-                        }
-                    }
-                    return ix;
-                }
-            });
-
-        });
-    }
-
-    function tick(e) {
-        network.numTicks++;
-
-        if (config.options.layering) {
-            // adjust the y-coord of each node, based on y-pos constraints
-            network.nodes.forEach(function (n) {
-                var z = e.alpha * n.constraint.weight;
-                if (!isNaN(n.constraint.y)) {
-                    n.y = (n.constraint.y * z + n.y * (1 - z));
-                }
-            });
-        }
-
-        if (config.options.collisionPrevention && network.preventCollisions) {
-            preventCollisions();
-        }
-
-        var portHalfW = config.labels.port.width / 2,
-            portHalfH = config.labels.port.height / 2;
-
-        // clip visualization of links at bounds of nodes...
-        network.link.each(function(d) {
-            var xs = d.source.x,
-                ys = d.source.y,
-                xt = d.target.x,
-                yt = d.target.y,
-                line = new geo.LineSegment(xs, ys, xt, yt),
-                e, ix,
-                exs, eys, ext, eyt,
-                pxs, pys, pxt, pyt;
-
-            if (d.class === 'host') {
-                // no adjustment for source end of link, since hosts are dots
-                exs = xs;
-                eys = ys;
-
-            } else {
-                for (e in d.source.edge) {
-                    ix = line.intersect(d.source.edge[e].offset(xs, ys));
-                    if (ix.in1 && ix.in2) {
-                        exs = ix.x;
-                        eys = ix.y;
-
-                        // also pick off the port label intersection
-                        ix = line.intersect(d.source.portEdge[e].offset(xs, ys));
-                        pxs = ix.x;
-                        pys = ix.y;
-                        break;
-                    }
-                }
-            }
-
-            for (e in d.target.edge) {
-                ix = line.intersect(d.target.edge[e].offset(xt, yt));
-                if (ix.in1 && ix.in2) {
-                    ext = ix.x;
-                    eyt = ix.y;
-
-                    // also pick off the port label intersection
-                    ix = line.intersect(d.target.portEdge[e].offset(xt, yt));
-                    pxt = ix.x;
-                    pyt = ix.y;
-                    break;
-                }
-            }
-
-            // adjust the endpoints of the link's line to match rectangles
-            var sid = safeId(d.id);
-            d3.select(this)
-                .attr('x1', exs)
-                .attr('y1', eys)
-                .attr('x2', ext)
-                .attr('y2', eyt);
-
-            d3.select('#srcPort-' + sid)
-                .attr('x', pxs - portHalfW)
-                .attr('y', pys - portHalfH);
-
-            d3.select('#tgtPort-' + sid)
-                .attr('x', pxt - portHalfW)
-                .attr('y', pyt - portHalfH);
-
-            // TODO: fit label rect to size of port number.
-            d3.select('#srcText-' + sid)
-                .attr('x', pxs - 5)
-                .attr('y', pys + 3);
-
-            d3.select('#tgtText-' + sid)
-                .attr('x', pxt - 5)
-                .attr('y', pyt + 3);
-
-        });
-
-        // position each node by translating the node (group) by x,y
-        network.node
-            .attr('transform', function(d) {
-                return translate(d.x, d.y);
-            });
-
-    }
-
-    //    $('#docs-close').on('click', function() {
-    //        deselectObject();
-    //        return false;
-    //    });
-
-    //    $(document).on('click', '.select-object', function() {
-    //        var obj = graph.data[$(this).data('name')];
-    //        if (obj) {
-    //            selectObject(obj);
-    //        }
-    //        return false;
-    //    });
-
-    function findNodeFromData(d) {
-        var el = null;
-        network.node.filter('.' + d.class).each(function(n) {
-            if (n.id === d.id) {
-                el = d3.select(this);
-            }
-        });
-        return el;
-    }
-
-    function selectObject(obj, el) {
-        var node;
-        if (el) {
-            node = d3.select(el);
-        } else {
-            network.node.each(function(d) {
-                if (d == obj) {
-                    node = d3.select(el = this);
-                }
-            });
-        }
-        if (!node) return;
-
-        if (node.classed('selected')) {
-            deselectObject();
-            flyinPane(null);
-            return;
-        }
-        deselectObject(false);
-
-        selected = {
-            obj : obj,
-            el  : el
-        };
-
-        node.classed('selected', true);
-        flyinPane(obj);
-    }
-
-    function deselectObject(doResize) {
-        // Review: logic of 'resize(...)' function.
-        if (doResize || typeof doResize == 'undefined') {
-            resize(false);
-        }
-
-        // deselect all nodes in the network...
-        network.node.classed('selected', false);
-        selected = {};
-        flyinPane(null);
-    }
-
-    function flyinPane(obj) {
-        var pane = d3.select('#flyout'),
-            url;
-
-        if (obj) {
-            // go get details of the selected object from the server...
-            url = detailJsonUrl(obj.id);
-            d3.json(url, function (err, data) {
-                if (err) {
-                    alert('Oops! Error reading JSON...\n\n' +
-                        'URL: ' + url + '\n\n' +
-                        'Error: ' + err.message);
-                    return;
-                }
-//                console.log("JSON data... " + url);
-//                console.log(data);
-
-                displayDetails(data, pane);
-            });
-
-        } else {
-            // hide pane
-            pane.transition().duration(750)
-                .style('right', '-320px')
-                .style('opacity', 0.0);
-        }
-    }
-
-    function displayDetails(data, pane) {
-        $('#flyout').empty();
-
-        var title = pane.append("h2"),
-            table = pane.append("table"),
-            tbody = table.append("tbody");
-
-        $('<img src="img/' + data.type + '.png">').appendTo(title);
-        $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
-
-
-        // TODO: consider using d3 data bind to TR/TD
-
-        data.propOrder.forEach(function(p) {
-            if (p === '-') {
-                addSep(tbody);
-            } else {
-                addProp(tbody, p, data.props[p]);
-            }
-        });
-
-        function addSep(tbody) {
-            var tr = tbody.append('tr');
-            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
-        }
-
-        function addProp(tbody, label, value) {
-            var tr = tbody.append('tr');
-
-            tr.append('td')
-                .attr('class', 'label')
-                .text(label + ' :');
-
-            tr.append('td')
-                .attr('class', 'value')
-                .text(value);
-        }
-
-        // show pane
-        pane.transition().duration(750)
-            .style('right', '20px')
-            .style('opacity', 1.0);
-    }
-
-    function highlightObject(obj) {
-        if (obj) {
-            if (obj != highlighted) {
-                // TODO set or clear "inactive" class on nodes, based on criteria
-                network.node.classed('inactive', function(d) {
-                    //                return (obj !== d &&
-                    //                    d.relation(obj.id));
-                    return (obj !== d);
-                });
-                // TODO: same with links
-                network.link.classed('inactive', function(d) {
-                    return (obj !== d.source && obj !== d.target);
-                });
-            }
-            highlighted = obj;
-        } else {
-            if (highlighted) {
-                // clear the inactive flag (no longer suppressed visually)
-                network.node.classed('inactive', false);
-                network.link.classed('inactive', false);
-            }
-            highlighted = null;
-
-        }
-    }
-
-    function hoverObject(obj) {
-        if (obj) {
-            hovered = obj;
-        } else {
-            if (hovered) {
-                hovered = null;
-            }
-        }
     }
 
 
-    function resize() {
-        view.height = window.innerHeight - config.mastHeight;
-        view.width = window.innerWidth;
-        $('#view')
-            .css('height', view.height + 'px')
-            .css('width', view.width + 'px');
+    function load(view, ctx) {
+        view.setRadio(btnSet, doRadio);
 
-        network.forceWidth = view.width - config.force.marginLR;
-        network.forceHeight = view.height - config.force.marginTB;
     }
 
-    // ======================================================================
-    // register with the UI framework
+    function resize(view, ctx) {
+        setSize(view);
+        setSize(view, bgImg);
+    }
+
+
+    // ==============================
+    // View registration
 
     onos.ui.addView('topo', {
-        load: loadNetworkView
+        preload: preload,
+        load: load,
+        resize: resize
     });
 
-
 }(ONOS));
-