ONOS-4971: Synthetic Link Data -- WIP, merge anyway

- created temp Topology2 View (topoX) to "process" and display topology data.
- made root layout parent of itself (just like /.. = /) to simplify layout hierarchy operations.
- added nodeType property to JSON rep of regions/devices/hosts.
- augmented peers to include devices.
- added skeleton topo2NavRegion event.

Change-Id: I8219125d7dfe33d211350ae27111a3d9de6eb4ca
diff --git a/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java b/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
index 287cc43..0036521 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
@@ -63,7 +63,7 @@
      * @param layoutId layout identifier
      * @return set of peer layouts; empty set if layout has no peers
      */
-    Set<UiTopoLayout> getPeers(UiTopoLayoutId layoutId);
+    Set<UiTopoLayout> getPeerLayouts(UiTopoLayoutId layoutId);
 
     /**
      * Returns the set of the child layouts of the specified layout.
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
index 604cf65..12c9de6 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayout.java
@@ -39,7 +39,8 @@
     public UiTopoLayout(UiTopoLayoutId id, Region region, UiTopoLayoutId parent) {
         this.id = id;
         this.region = region;
-        this.parent = parent;
+        // NOTE: root layout is its own parent...
+        this.parent = parent != null ? parent : this.id;
     }
 
     @Override
@@ -85,5 +86,13 @@
         return parent;
     }
 
-    // TODO: additional properties pertinent to the layout
+    /**
+     * Returns true if this layout instance is at the top of the
+     * hierarchy tree.
+     *
+     * @return true if this is the root layout
+     */
+    public boolean isRoot() {
+        return id.equals(parent);
+    }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 79f3d77..0cbc848 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -134,6 +134,7 @@
 
                 // FIXME: leave commented out for now, while still under development
 //                new UiView(NETWORK, "topo2", "New-Topo"),
+//                new UiView(NETWORK, "topoX", "Topo-X"),
 
                 new UiView(NETWORK, "device", "Devices", "nav_devs"),
                 new UiViewHidden("flow"),
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
index 02df84c..8a26a92 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
@@ -40,6 +40,8 @@
 import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -62,6 +64,12 @@
     private static final String E_UNKNOWN_UI_NODE =
             "Unknown subclass of UiNode: ";
 
+    private static final String REGION = "region";
+    private static final String DEVICE = "device";
+    private static final String HOST = "host";
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
     private final ObjectMapper mapper = new ObjectMapper();
 
     private ServiceDirectory directory;
@@ -245,6 +253,7 @@
     private ObjectNode json(UiDevice device) {
         ObjectNode node = objectNode()
                 .put("id", device.idAsString())
+                .put("nodeType", DEVICE)
                 .put("type", device.type())
                 .put("online", device.isOnline())
                 .put("master", nullIsEmpty(device.master()))
@@ -266,6 +275,7 @@
     private ObjectNode json(UiHost host) {
         return objectNode()
                 .put("id", host.idAsString())
+                .put("nodeType", HOST)
                 .put("layer", host.layer());
         // TODO: complete host details
     }
@@ -281,10 +291,31 @@
     private ObjectNode jsonClosedRegion(UiRegion region) {
         return objectNode()
                 .put("id", region.idAsString())
+                .put("nodeType", REGION)
                 .put("nDevs", region.deviceCount());
         // TODO: complete closed-region details
     }
 
+    /**
+     * Returns a JSON array representation of a set of regions/devices. Note
+     * that the information is sufficient for showing regions as nodes.
+     *
+     * @param nodes the nodes
+     * @return a JSON representation of the nodes
+     */
+    public ArrayNode closedNodes(Set<UiNode> nodes) {
+        ArrayNode array = arrayNode();
+        for (UiNode node: nodes) {
+            if (node instanceof UiRegion) {
+                array.add(jsonClosedRegion((UiRegion) node));
+            } else if (node instanceof UiDevice) {
+                array.add(json((UiDevice) node));
+            } else {
+                log.warn("Unexpected node instance: {}", node.getClass());
+            }
+        }
+        return array;
+    }
 
     /**
      * Returns a JSON array representation of a list of regions. Note that the
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2ViewMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2ViewMessageHandler.java
index 5449d93..7795271 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2ViewMessageHandler.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2ViewMessageHandler.java
@@ -24,6 +24,7 @@
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.impl.UiWebSocket;
 import org.onosproject.ui.model.topo.UiClusterMember;
+import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
@@ -55,8 +56,9 @@
     private final Logger log = LoggerFactory.getLogger(getClass());
 
     // === Inbound event identifiers
-    private static final String TOPO2_START = "topo2Start";
-    private static final String TOPO2_STOP = "topo2Stop";
+    private static final String START = "topo2Start";
+    private static final String NAV_REGION = "topo2navRegion";
+    private static final String STOP = "topo2Stop";
 
     // === Outbound event identifiers
     private static final String ALL_INSTANCES = "topo2AllInstances";
@@ -83,6 +85,7 @@
     protected Collection<RequestHandler> createRequestHandlers() {
         return ImmutableSet.of(
                 new Topo2Start(),
+                new Topo2NavRegion(),
                 new Topo2Stop()
         );
     }
@@ -92,7 +95,7 @@
 
     private final class Topo2Start extends RequestHandler {
         private Topo2Start() {
-            super(TOPO2_START);
+            super(START);
         }
 
         @Override
@@ -124,10 +127,10 @@
             Set<UiRegion> kids = topoSession.getSubRegions(currentLayout);
             sendMessage(CURRENT_REGION, t2json.region(region, kids));
 
-            // these are the regions that are siblings to this one
-            Set<UiRegion> peers = topoSession.getPeerRegions(currentLayout);
+            // these are the regions/devices that are siblings to this region
+            Set<UiNode> peers = topoSession.getPeerNodes(currentLayout);
             ObjectNode peersPayload = objectNode();
-            peersPayload.set("peers", t2json.closedRegions(peers));
+            peersPayload.set("peers", t2json.closedNodes(peers));
             sendMessage(PEER_REGIONS, peersPayload);
 
             // finally, tell the UI that we are done : TODO review / delete??
@@ -146,9 +149,22 @@
 
     }
 
+    private final class Topo2NavRegion extends RequestHandler {
+        private Topo2NavRegion() {
+            super(NAV_REGION);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            String dir = string(payload, "dir");
+            String rid = string(payload, "rid");
+            log.debug("NavRegion: dir={}, rid={}", dir, rid);
+        }
+    }
+
     private final class Topo2Stop extends RequestHandler {
         private Topo2Stop() {
-            super(TOPO2_STOP);
+            super(STOP);
         }
 
         @Override
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
index 28e345c..fcf4c46 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoLayoutManager.java
@@ -111,15 +111,17 @@
     }
 
     @Override
-    public Set<UiTopoLayout> getPeers(UiTopoLayoutId layoutId) {
+    public Set<UiTopoLayout> getPeerLayouts(UiTopoLayoutId layoutId) {
         checkNotNull(layoutId, ID_NULL);
+
         UiTopoLayout layout = layoutMap.get(layoutId);
-        if (layout == null) {
+        if (layout == null || layout.isRoot()) {
             return Collections.emptySet();
         }
 
         UiTopoLayoutId parentId = layout.parent();
         return layoutMap.values().stream()
+                // all layouts who are NOT me and who share my parent...
                 .filter(l -> !Objects.equals(l.id(), layoutId) &&
                         Objects.equals(l.parent(), parentId))
                 .collect(Collectors.toSet());
@@ -129,7 +131,7 @@
     public Set<UiTopoLayout> getChildren(UiTopoLayoutId layoutId) {
         checkNotNull(layoutId, ID_NULL);
         return layoutMap.values().stream()
-                .filter(l -> Objects.equals(l.parent(), layoutId))
+                .filter(l -> !l.isRoot() && Objects.equals(l.parent(), layoutId))
                 .collect(Collectors.toSet());
     }
 
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
index 47907d4..7127ec1 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/UiTopoSession.java
@@ -23,6 +23,7 @@
 import org.onosproject.ui.impl.topo.model.UiModelListener;
 import org.onosproject.ui.impl.topo.model.UiSharedTopologyModel;
 import org.onosproject.ui.model.topo.UiClusterMember;
+import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
@@ -170,17 +171,32 @@
     }
 
     /**
-     * Returns the regions that are "peers" to this region. That is, based on
-     * the layout the user is viewing, all the regions that are associated with
-     * layouts that share the same parent layout as this layout.
+     * Returns the regions/devices that are "peers" to this region. That is,
+     * based on the layout the user is viewing, all the regions/devices that
+     * are associated with layouts that share the same parent layout as this
+     * layout, AND that are linked to an element within this region.
      *
      * @param layout the layout being viewed
-     * @return all regions that are "siblings" to this layout's region
+     * @return all regions/devices that are "siblings" to this layout's region
      */
-    public Set<UiRegion> getPeerRegions(UiTopoLayout layout) {
-        Set<UiTopoLayout> peerLayouts = layoutService.getPeers(layout.id());
-        Set<UiRegion> peers = new HashSet<>();
-        peerLayouts.forEach(l -> peers.add(sharedModel.getRegion(l.regionId())));
+    public Set<UiNode> getPeerNodes(UiTopoLayout layout) {
+        Set<UiNode> peers = new HashSet<>();
+
+        // first, get the peer regions
+        Set<UiTopoLayout> peerLayouts = layoutService.getPeerLayouts(layout.id());
+        peerLayouts.forEach(l -> {
+            RegionId peerRegion = l.regionId();
+            peers.add(sharedModel.getRegion(peerRegion));
+        });
+
+        // now add the devices that reside in the parent region
+        if (!layout.isRoot()) {
+            UiTopoLayout parentLayout = layoutService.getLayout(layout.parent());
+            getRegion(parentLayout).devices().forEach(peers::add);
+        }
+
+        // TODO: Finally, filter out regions / devices that are not connected
+        //       directly to this region by an implicit link
         return peers;
     }
 
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.css b/web/gui/src/main/webapp/app/view/topo2/topo2.css
index 7f878a5..6ec4f02 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.css
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.css
@@ -25,3 +25,28 @@
     /* prevents the little cut/copy/paste square that would appear on iPad */
     -webkit-user-select: none;
 }
+
+/* -- TEMPORARY CSS (to be deleted) -- */
+#topo2tmp div {
+    padding: 8px 24px;
+    margin: 8px;
+    background-color: #ddddff;
+}
+#topo2tmp div div {
+    padding: 4px 10px;
+}
+
+#topo2tmp h4 {
+    margin: 0
+}
+#topo2tmp p {
+    margin: 0
+}
+#topo2tmp .nav-me:hover {
+    background-color: #bbbbdd;
+}
+#topo2tmp .nav-me {
+    font-weight: bold;
+    text-decoration: underline;
+    cursor: pointer;
+}
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2.html b/web/gui/src/main/webapp/app/view/topo2/topo2.html
index 9b987a6..1f0c6d6 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2.html
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2.html
@@ -1,5 +1,36 @@
 <!-- Topology View partial HTML -->
 <div id="ov-topo2">
+    <div id="topo2tmp">
+        <div class="parentRegion">
+            Parent Region: <span> - </span>
+        </div>
+        <div class="thisRegion">
+            This Region: <span> - </span>
+        </div>
+        <div class="subRegions">
+            <h4>Subregions</h4>
+            <div></div>
+        </div>
+        <div class="devices">
+            <h4>Devices</h4>
+            <div></div>
+        </div>
+        <div class="hosts">
+            <h4>Hosts</h4>
+            <div></div>
+        </div>
+        <div class="links">
+            <h4>Links</h4>
+            <div></div>
+        </div>
+        <div class="peers">
+            <h4>Peers</h4>
+            <div></div>
+        </div>
+    </div>
+
+    <!-- Below here is good; Above here is temporary, for debugging -->
+
     <svg viewBox="0 0 1000 1000"
          resize offset-height="56" offset-width="12"
          notifier="notifyResize()">
diff --git a/web/gui/src/main/webapp/app/view/topo2/topo2Event.js b/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
index 83dcae2..2d13a5c 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Event.js
@@ -42,6 +42,7 @@
             topo2AllInstances: t2fs,
             topo2CurrentLayout: t2fs,
             topo2CurrentRegion: t2fs,
+            topo2PeerRegions: t2fs,
             topo2StartDone: t2fs
 
             // Add further event names / module references as needed
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 db6a1c2..254f176 100644
--- a/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
+++ b/web/gui/src/main/webapp/app/view/topo2/topo2Force.js
@@ -35,10 +35,79 @@
         $log.debug('Destroy topo force layout');
     }
 
+    // ========================== Temporary Code (to be deleted later)
+
+    function request(dir, rid) {
+        wss.sendEvent('topo2navRegion', {
+            dir: dir,
+            rid: rid
+        });
+    }
+
+    function doTmpCurrentLayout(data) {
+        var topdiv = d3.select('#topo2tmp');
+        var parentRegion = data.parent;
+        var span = topdiv.select('.parentRegion').select('span');
+        span.text(parentRegion || '[no parent]');
+        span.classed('nav-me', !!parentRegion);
+    }
+
+    function doTmpCurrentRegion(data) {
+        var topdiv = d3.select('#topo2tmp');
+        var span = topdiv.select('.thisRegion').select('span');
+        var div;
+
+        span.text(data.id);
+
+        div = topdiv.select('.subRegions').select('div');
+        data.subregions.forEach(function (r) {
+
+            function nav() {
+                request('down', r.id);
+            }
+
+            div.append('p')
+                .classed('nav-me', true)
+                .text(r.id)
+                .on('click', nav);
+        });
+
+        div = topdiv.select('.devices').select('div');
+        data.layerOrder.forEach(function (tag, idx) {
+            var devs = data.devices[idx];
+            devs.forEach(function (d) {
+                div.append('p')
+                    .text('[' + tag + '] ' + d.id);
+            });
+
+        });
+
+        div = topdiv.select('.hosts').select('div');
+        data.layerOrder.forEach(function (tag, idx) {
+            var hosts = data.hosts[idx];
+            hosts.forEach(function (h) {
+                div.append('p')
+                    .text('[' + tag + '] ' + h.id);
+            });
+        });
+
+        div = topdiv.select('.links').select('div');
+        var links = data.links;
+        links.forEach(function (lnk) {
+            div.append('p')
+                .text(lnk.id);
+        });
+    }
+
+    function doTmpPeerRegions(data) {
+
+    }
+
     // ========================== Event Handlers
 
     function allInstances(data) {
         $log.debug('>> topo2AllInstances event:', data)
+        doTmpCurrentLayout(data);
     }
 
     function currentLayout(data) {
@@ -47,6 +116,16 @@
 
     function currentRegion(data) {
         $log.debug('>> topo2CurrentRegion event:', data)
+        doTmpCurrentRegion(data);
+    }
+
+    function topo2PeerRegions(data) {
+        $log.debug('>> topo2PeerRegions event:', data)
+        doTmpPeerRegions(data);
+    }
+
+    function topo2PeerRegions(data) {
+        $log.debug('>> topo2PeerRegions event:', data)
     }
 
     function startDone(data) {
@@ -69,6 +148,7 @@
                 topo2AllInstances: allInstances,
                 topo2CurrentLayout: currentLayout,
                 topo2CurrentRegion: currentRegion,
+                topo2PeerRegions: topo2PeerRegions,
                 topo2StartDone: startDone
             };
         }]);
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoX-theme.css b/web/gui/src/main/webapp/app/view/topoX/topoX-theme.css
new file mode 100644
index 0000000..af95a40
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoX-theme.css
@@ -0,0 +1,62 @@
+/*
+ * 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 (theme) -- CSS file
+ */
+
+/* --- Base SVG Layer --- */
+
+#ov-topoX svg {
+    /*background-color: #f4f4f4;*/
+    background-color: goldenrod; /* just for testing */
+}
+
+/* --- "No Devices" Layer --- */
+
+#ov-topoX svg .noDevsBird {
+    fill: #db7773;
+}
+
+#ov-topoX svg #topoX-noDevsLayer text {
+    fill: #7e9aa8;
+}
+
+/* --- Topo Map --- */
+
+#ov-topoX svg #topoX-map {
+    stroke-width: 2px;
+    stroke: #f4f4f4;
+    fill: #e5e5e6;
+}
+
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoX.css b/web/gui/src/main/webapp/app/view/topoX/topoX.css
new file mode 100644
index 0000000..26661b0
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoX.css
@@ -0,0 +1,52 @@
+/*
+ * 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 (layout) -- CSS file
+ */
+
+/* --- Base SVG Layer --- */
+
+#ov-topoX svg {
+    /* prevents the little cut/copy/paste square that would appear on iPad */
+    -webkit-user-select: none;
+}
+
+/* -- TEMPORARY CSS (to be deleted) -- */
+#topoXtmp div {
+    padding: 8px 24px;
+    margin: 8px;
+    background-color: #ddddff;
+}
+#topoXtmp div div {
+    padding: 4px 10px;
+}
+
+#topoXtmp h4 {
+    margin: 0
+}
+#topoXtmp p {
+    margin: 0
+}
+#topoXtmp .nav-me:hover {
+    background-color: #bbbbdd;
+}
+#topoXtmp .nav-me {
+    font-weight: bold;
+    text-decoration: underline;
+    cursor: pointer;
+}
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoX.html b/web/gui/src/main/webapp/app/view/topoX/topoX.html
new file mode 100644
index 0000000..41a94a5
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoX.html
@@ -0,0 +1,47 @@
+<!--
+  ~ 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.
+  -->
+
+<!-- Topology View partial HTML -->
+<div id="ov-topoX">
+    <div id="topoXtmp">
+        <div class="parentRegion">
+            Parent Region: <span> - </span>
+        </div>
+        <div class="thisRegion">
+            This Region: <span> - </span>
+        </div>
+        <div class="subRegions">
+            <h4>Subregions</h4>
+            <div></div>
+        </div>
+        <div class="devices">
+            <h4>Devices</h4>
+            <div></div>
+        </div>
+        <div class="hosts">
+            <h4>Hosts</h4>
+            <div></div>
+        </div>
+        <div class="links">
+            <h4>Links</h4>
+            <div></div>
+        </div>
+        <div class="peers">
+            <h4>Peers</h4>
+            <div></div>
+        </div>
+    </div>
+</div>
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoX.js b/web/gui/src/main/webapp/app/view/topoX/topoX.js
new file mode 100644
index 0000000..88114c8
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoX.js
@@ -0,0 +1,214 @@
+/*
+ * 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
+
+ NOTE: currently under development to support Regions.
+ */
+
+(function () {
+    'use strict';
+
+    // references to injected services
+    var $scope, $log, $loc,
+        fs, mast, ks, zs,
+        gs, ms, sus, flash,
+        wss, ps, th,
+        tXes, tXfs;
+
+    // DOM elements
+    var ovtopoX, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
+
+    // Internal state
+    var zoomer, actionMap;
+
+
+    // === Helper Functions
+
+    // callback invoked when the SVG view has been resized..
+    function svgResized(s) {
+        $log.debug("topoX view resized", s);
+    }
+
+    function setUpKeys(overlayKeys) {
+        $log.debug('topoX: set up keys....');
+    }
+
+    // === Controller Definition -----------------------------------------
+
+    angular.module('ovTopoX', ['onosUtil', 'onosSvg', 'onosRemote'])
+    .controller('OvTopoXCtrl',
+        ['$scope', '$log', '$location',
+        'FnService', 'MastService', 'KeyService', 'ZoomService',
+        'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
+        'WebSocketService', 'PrefsService', 'ThemeService',
+        'TopoXEventService', 'TopoXForceService',
+
+        function (_$scope_, _$log_, _$loc_,
+                  _fs_, _mast_, _ks_, _zs_,
+                  _gs_, _ms_, _sus_, _flash_,
+                  _wss_, _ps_, _th_,
+                  _tXes_, _tXfs_) {
+
+            var params = _$loc_.search(),
+                projection,
+                dim,
+                wh,
+                uplink = {
+                    // provides function calls back into this space
+                    // showNoDevs: showNoDevs,
+                    // projection: function () { return projection; },
+                    // zoomLayer: function () { return zoomLayer; },
+                    // zoomer: function () { return zoomer; },
+                    // opacifyMap: opacifyMap,
+                    // topoStartDone: topoStartDone
+                };
+
+            $scope = _$scope_;
+            $log = _$log_;
+            $loc = _$loc_;
+
+            fs = _fs_;
+            mast = _mast_;
+            ks = _ks_;
+            zs = _zs_;
+
+            gs = _gs_;
+            ms = _ms_;
+            sus = _sus_;
+            flash = _flash_;
+
+            wss = _wss_;
+            ps = _ps_;
+            th = _th_;
+            
+            tXes = _tXes_;
+            tXfs = _tXfs_;
+
+            // capture selected intent parameters (if they are set in the
+            //  query string) so that the traffic overlay can highlight
+            //  the path for that intent
+            if (params.intentKey && params.intentAppId && params.intentAppName) {
+                $scope.intentData = {
+                    key: params.intentKey,
+                    appId: params.intentAppId,
+                    appName: params.intentAppName
+                };
+            }
+
+            $scope.notifyResize = function () {
+                svgResized(fs.windowSize(mast.mastHeight()));
+            };
+
+            // Cleanup on destroyed scope..
+            $scope.$on('$destroy', function () {
+                $log.log('OvTopoXCtrl is saying Buh-Bye!');
+                tXes.stop();
+                ks.unbindKeys();
+                tXfs.destroy();
+            });
+
+            // svg layer and initialization of components
+            ovtopoX = d3.select('#ov-topoX');
+            svg = ovtopoX.select('svg');
+            // set the svg size to match that of the window, less the masthead
+            wh = fs.windowSize(mast.mastHeight());
+            $log.debug('setting topo SVG size to', wh);
+            svg.attr(wh);
+            dim = [wh.width, wh.height];
+
+
+            // set up our keyboard shortcut bindings
+            setUpKeys();
+
+            // make sure we can respond to topology events from the server
+            tXes.bindHandlers();
+
+            // initialize the force layout, ready to render the topology
+            tXfs.init();
+
+
+            // =-=-=-=-=-=-=-=-
+            // TODO: in future, we will load background map data
+            //  asynchronously (hence the promise) and then chain off 
+            //  there to send the topoXstart event to the server.
+            // For now, we'll send the event inline...
+            tXes.start();
+
+            
+            // === ORIGINAL CODE ===
+            
+            // setUpKeys();
+            // setUpToolbar();
+            // setUpDefs();
+            // setUpZoom();
+            // setUpNoDevs();
+            /*
+            setUpMap().then(
+                function (proj) {
+                    var z = ps.getPrefs('topo_zoom', { tx:0, ty:0, sc:1 });
+                    zoomer.panZoom([z.tx, z.ty], z.sc);
+                    $log.debug('** Zoom restored:', z);
+
+                    projection = proj;
+                    $log.debug('** We installed the projection:', proj);
+                    flash.enable(false);
+                    toggleMap(prefsState.bg);
+                    flash.enable(true);
+                    mapShader(true);
+
+                    // now we have the map projection, we are ready for
+                    //  the server to send us device/host data...
+                    tes.start();
+                    // need to do the following so we immediately get
+                    //  the summary panel data back from the server
+                    restoreSummaryFromPrefs();
+                }
+            );
+            */
+            // tes.bindHandlers();
+            // setUpSprites();
+
+            // forceG = zoomLayer.append('g').attr('id', 'topo-force');
+            // tfs.initForce(svg, forceG, uplink, dim);
+            // tis.initInst({ showMastership: tfs.showMastership });
+            // tps.initPanels();
+
+            // restoreConfigFromPrefs();
+            // ttbs.setDefaultOverlay(prefsState.ovidx);
+
+            // $log.debug('registered overlays...', tov.list());
+            
+            $log.log('OvTopoXCtrl has been created');
+        }]);
+}());
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoXEvent.js b/web/gui/src/main/webapp/app/view/topoX/topoXEvent.js
new file mode 100644
index 0000000..a9f7a99
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoXEvent.js
@@ -0,0 +1,101 @@
+/*
+ * 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 Event Module.
+
+ Defines the conduit between the client and the server:
+ - provides a clean API for sending events to the server
+ - dispatches incoming events from the server to the appropriate sub-module
+
+ */
+
+(function () {
+    'use strict';
+
+    // injected refs
+    var $log, wss, tXfs;
+
+    // internal state
+    var handlerMap,
+        openListener;
+
+    // ========================== Helper Functions
+
+    function createHandlerMap() {
+        handlerMap = {
+            topo2AllInstances: tXfs,
+            topo2CurrentLayout: tXfs,
+            topo2CurrentRegion: tXfs,
+            topo2PeerRegions: tXfs,
+            topo2StartDone: tXfs
+
+            // Add further event names / module references as needed
+        };
+    }
+
+    function wsOpen(host, url) {
+        $log.debug('topoXEvent: WSopen - cluster node:', host, 'URL:', url);
+        // tell the server we are ready to receive topo events
+        wss.sendEvent('topo2Start');
+    }
+
+    // bind our event handlers to the web socket service, so that our
+    //  callbacks get invoked for incoming events
+    function bindHandlers() {
+        wss.bindHandlers(handlerMap);
+        $log.debug('topoX event handlers bound');
+    }
+
+    // tell the server we are ready to receive topology events
+    function start() {
+        // in case we fail over to a new server,
+        // listen for wsock-open events
+        openListener = wss.addOpenListener(wsOpen);
+        wss.sendEvent('topo2Start');
+        $log.debug('topoX comms started');
+    }
+
+    // tell the server we no longer wish to receive topology events
+    function stop() {
+        wss.sendEvent('topo2Stop');
+        wss.unbindHandlers(handlerMap);
+        wss.removeOpenListener(openListener);
+        openListener = null;
+        $log.debug('topoX comms stopped');
+    }
+
+    // ========================== Main Service Definition
+
+    angular.module('ovTopoX')
+    .factory('TopoXEventService',
+        ['$log', 'WebSocketService', 'TopoXForceService',
+
+        function (_$log_, _wss_, _tXfs_) {
+            $log = _$log_;
+            wss = _wss_;
+            tXfs = _tXfs_;
+
+            // deferred creation of handler map, so module references are good
+            createHandlerMap();
+
+            return {
+                bindHandlers: bindHandlers,
+                start: start,
+                stop: stop
+            };
+        }]);
+}());
diff --git a/web/gui/src/main/webapp/app/view/topoX/topoXForce.js b/web/gui/src/main/webapp/app/view/topoX/topoXForce.js
new file mode 100644
index 0000000..9797a45
--- /dev/null
+++ b/web/gui/src/main/webapp/app/view/topoX/topoXForce.js
@@ -0,0 +1,151 @@
+/*
+ * 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';
+
+    // injected refs
+    var $log, wss;
+
+    // ========================== Helper Functions
+
+    function init() {
+        $log.debug('Initialize topo force layout');
+    }
+
+    function destroy() {
+        $log.debug('Destroy topo force layout');
+    }
+
+    // ========================== Temporary Code (to be deleted later)
+
+    function request(dir, rid) {
+        wss.sendEvent('topo2navRegion', {
+            dir: dir,
+            rid: rid
+        });
+    }
+
+    function doTmpCurrentLayout(data) {
+        var topdiv = d3.select('#topoXtmp');
+        var parentRegion = data.parent;
+        var span = topdiv.select('.parentRegion').select('span');
+        span.text(parentRegion || '[no parent]');
+        span.classed('nav-me', !!parentRegion);
+    }
+
+    function doTmpCurrentRegion(data) {
+        var topdiv = d3.select('#topoXtmp');
+        var span = topdiv.select('.thisRegion').select('span');
+        var div;
+
+        span.text(data.id);
+
+        div = topdiv.select('.subRegions').select('div');
+        data.subregions.forEach(function (r) {
+
+            function nav() {
+                request('down', r.id);
+            }
+
+            div.append('p')
+                .classed('nav-me', true)
+                .text(r.id)
+                .on('click', nav);
+        });
+
+        div = topdiv.select('.devices').select('div');
+        data.layerOrder.forEach(function (tag, idx) {
+            var devs = data.devices[idx];
+            devs.forEach(function (d) {
+                div.append('p')
+                    .text('[' + tag + '] ' + d.id);
+            });
+
+        });
+
+        div = topdiv.select('.hosts').select('div');
+        data.layerOrder.forEach(function (tag, idx) {
+            var hosts = data.hosts[idx];
+            hosts.forEach(function (h) {
+                div.append('p')
+                    .text('[' + tag + '] ' + h.id);
+            });
+        });
+
+        div = topdiv.select('.links').select('div');
+        var links = data.links;
+        links.forEach(function (lnk) {
+            div.append('p')
+                .text(lnk.id);
+        });
+    }
+
+    function doTmpPeerRegions(data) {
+
+    }
+
+    // ========================== Event Handlers
+
+    function allInstances(data) {
+        $log.debug('>> topo2AllInstances event:', data)
+        doTmpCurrentLayout(data);
+    }
+
+    function currentLayout(data) {
+        $log.debug('>> topo2CurrentLayout event:', data)
+    }
+
+    function currentRegion(data) {
+        $log.debug('>> topo2CurrentRegion event:', data)
+        doTmpCurrentRegion(data);
+    }
+
+    function peerRegions(data) {
+        $log.debug('>> topo2PeerRegions event:', data)
+        doTmpPeerRegions(data);
+    }
+
+    function startDone(data) {
+        $log.debug('>> topo2StartDone event:', data)
+    }
+    
+    // ========================== Main Service Definition
+
+    angular.module('ovTopoX')
+    .factory('TopoXForceService',
+        ['$log', 'WebSocketService',
+
+        function (_$log_, _wss_) {
+            $log = _$log_;
+            wss = _wss_;
+            
+            return {
+                init: init,
+                destroy: destroy,
+                topo2AllInstances: allInstances,
+                topo2CurrentLayout: currentLayout,
+                topo2CurrentRegion: currentRegion,
+                topo2PeerRegions: peerRegions,
+                topo2StartDone: startDone
+            };
+        }]);
+}());
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index 22865a1..c95597b 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -133,6 +133,12 @@
     <link rel="stylesheet" href="app/view/topo2/topo2.css">
     <link rel="stylesheet" href="app/view/topo2/topo2-theme.css">
 
+    <script src="app/view/topoX/topoX.js"></script>
+    <script src="app/view/topoX/topoXEvent.js"></script>
+    <script src="app/view/topoX/topoXForce.js"></script>
+    <link rel="stylesheet" href="app/view/topoX/topoX.css">
+    <link rel="stylesheet" href="app/view/topoX/topoX-theme.css">
+
     <!-- Builtin views javascript. -->
     <script src="app/view/topo/topo.js"></script>
     <script src="app/view/topo/topoD3.js"></script>
diff --git a/web/gui/src/test/_karma/ev/topo2a/ev_3_currentRegion.json b/web/gui/src/test/_karma/ev/topo2a/ev_3_currentRegion.json
index dcf5086..b8b0c8a 100644
--- a/web/gui/src/test/_karma/ev/topo2a/ev_3_currentRegion.json
+++ b/web/gui/src/test/_karma/ev/topo2a/ev_3_currentRegion.json
@@ -4,9 +4,11 @@
     "id": "<null-region>",
     "subregions": [{
       "id": "r2",
+      "nodeType":"region",
       "nDevs": 2
     }, {
       "id": "r1",
+      "nodeType":"region",
       "nDevs": 3
     }],
     "devices": [
@@ -14,6 +16,7 @@
       [],
       [{
         "id": "null:0000000000000001",
+        "nodeType":"device",
         "type": "switch",
         "online": false,
         "master": "",