ONOS-4326: TopoRegions: Implement basic structure of response to 'topo2Start' event.
- this is WIP: still need to extract data from model cache.

Change-Id: I5ab843a1c352275a8da89964c886b660e3b8b616
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiNode.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiNode.java
index 79fdf5a..bc9fbf5 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiNode.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiNode.java
@@ -19,7 +19,7 @@
 /**
  * Represents a node drawn on the topology view (region, device, host).
  */
-abstract class UiNode extends UiElement {
+public abstract class UiNode extends UiElement {
 
     /**
      * Default "layer" tag.
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 be19389..df6dba9 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
@@ -37,12 +37,19 @@
 import org.onosproject.ui.model.topo.UiDevice;
 import org.onosproject.ui.model.topo.UiHost;
 import org.onosproject.ui.model.topo.UiLink;
+import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 
 /**
  * Facility for creating JSON messages to send to the topology view in the
@@ -50,6 +57,11 @@
  */
 class Topo2Jsonifier {
 
+    private static final String E_DEF_NOT_LAST =
+            "UiNode.LAYER_DEFAULT not last in layer list";
+    private static final String E_UNKNOWN_UI_NODE =
+            "Unknown subclass of UiNode: ";
+
     private final ObjectMapper mapper = new ObjectMapper();
 
     private ServiceDirectory directory;
@@ -87,7 +99,10 @@
         portStatsService = directory.get(PortStatisticsService.class);
         topologyService = directory.get(TopologyService.class);
         tunnelService = directory.get(TunnelService.class);
+    }
 
+    // for unit testing
+    Topo2Jsonifier() {
     }
 
     private ObjectNode objectNode() {
@@ -155,47 +170,104 @@
      * Returns a JSON representation of the region to display in the topology
      * view.
      *
-     * @param region the region to transform to JSON
+     * @param region     the region to transform to JSON
+     * @param subRegions the subregions within this region
      * @return a JSON representation of the data
      */
-    ObjectNode region(UiRegion region) {
+    ObjectNode region(UiRegion region, Set<UiRegion> subRegions) {
         ObjectNode payload = objectNode();
-
         if (region == null) {
             payload.put("note", "no-region");
             return payload;
         }
+        payload.put("id", region.idAsString());
+        payload.set("subregions", jsonSubRegions(subRegions));
 
-        payload.put("id", region.id().toString());
+        List<String> layerTags = region.layerOrder();
+        List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
+        List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
+        Set<UiLink> links = region.links();
 
-        ArrayNode layerOrder = arrayNode();
-        payload.set("layerOrder", layerOrder);
-        region.layerOrder().forEach(layerOrder::add);
-
-        ArrayNode devices = arrayNode();
-        payload.set("devices", devices);
-        for (UiDevice device : region.devices()) {
-            devices.add(json(device));
-        }
-
-        ArrayNode hosts = arrayNode();
-        payload.set("hosts", hosts);
-        for (UiHost host : region.hosts()) {
-            hosts.add(json(host));
-        }
-
-        ArrayNode links = arrayNode();
-        payload.set("links", links);
-        for (UiLink link : region.links()) {
-            links.add(json(link));
-        }
+        payload.set("devices", jsonGrouped(splitDevices));
+        payload.set("hosts", jsonGrouped(splitHosts));
+        payload.set("links", jsonLinks(links));
+        payload.set("layerOrder", jsonStrings(layerTags));
 
         return payload;
     }
 
+    private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
+        ArrayNode kids = arrayNode();
+        if (subregions != null) {
+            subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
+        }
+        return kids;
+    }
+
+    private ArrayNode jsonStrings(List<String> strings) {
+        ArrayNode array = arrayNode();
+        strings.forEach(array::add);
+        return array;
+    }
+
+    private ArrayNode jsonLinks(Set<UiLink> links) {
+        ArrayNode result = arrayNode();
+        links.forEach(lnk -> result.add(json(lnk)));
+        return result;
+    }
+
+    private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
+        ArrayNode result = arrayNode();
+        groupedNodes.forEach(g -> {
+            ArrayNode subset = arrayNode();
+            g.forEach(n -> subset.add(json(n)));
+            result.add(subset);
+        });
+        return result;
+    }
+
+    /**
+     * Returns a JSON payload that encapsulates the devices, hosts, links that
+     * do not belong to any region.
+     *
+     * @param oDevices  orphan devices
+     * @param oHosts    orphan hosts
+     * @param oLinks    orphan links
+     * @param layerTags layer tags
+     * @return a JSON representation of the data
+     */
+    ObjectNode orphans(Set<UiDevice> oDevices, Set<UiHost> oHosts,
+                       Set<UiLink> oLinks, List<String> layerTags) {
+
+        ObjectNode payload = objectNode();
+
+        List<Set<UiNode>> splitDevices = splitByLayer(layerTags, oDevices);
+        List<Set<UiNode>> splitHosts = splitByLayer(layerTags, oHosts);
+
+        payload.set("devices", jsonGrouped(splitDevices));
+        payload.set("hosts", jsonGrouped(splitHosts));
+        payload.set("links", jsonLinks(oLinks));
+        payload.set("layerOrder", jsonStrings(layerTags));
+
+        return payload;
+    }
+
+    private ObjectNode json(UiNode node) {
+        if (node instanceof UiRegion) {
+            return jsonClosedRegion((UiRegion) node);
+        }
+        if (node instanceof UiDevice) {
+            return json((UiDevice) node);
+        }
+        if (node instanceof UiHost) {
+            return json((UiHost) node);
+        }
+        throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
+    }
+
     private ObjectNode json(UiDevice device) {
         ObjectNode node = objectNode()
-                .put("id", device.id().toString())
+                .put("id", device.idAsString())
                 .put("type", device.type())
                 .put("online", device.isOnline())
                 .put("master", device.master().toString())
@@ -216,7 +288,7 @@
 
     private ObjectNode json(UiHost host) {
         return objectNode()
-                .put("id", host.id().toString())
+                .put("id", host.idAsString())
                 .put("layer", host.layer());
         // TODO: complete host details
     }
@@ -224,9 +296,101 @@
 
     private ObjectNode json(UiLink link) {
         return objectNode()
-                .put("id", link.id().toString());
+                .put("id", link.idAsString());
         // TODO: complete link details
     }
 
 
+    private ObjectNode jsonClosedRegion(UiRegion region) {
+        return objectNode()
+                .put("id", region.idAsString());
+        // TODO: complete closed-region details
+    }
+
+
+    /**
+     * Returns a JSON array representation of a list of regions. Note that the
+     * information about each region is limited to what needs to be used to
+     * show the regions as nodes on the view.
+     *
+     * @param regions the regions
+     * @return a JSON representation of the minimal region information
+     */
+    public ArrayNode closedRegions(Set<UiRegion> regions) {
+        ArrayNode array = arrayNode();
+        for (UiRegion r : regions) {
+            array.add(jsonClosedRegion(r));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of devices.
+     *
+     * @param devices the devices
+     * @return a JSON representation of the devices
+     */
+    public ArrayNode devices(Set<UiDevice> devices) {
+        ArrayNode array = arrayNode();
+        for (UiDevice device : devices) {
+            array.add(json(device));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of hosts.
+     *
+     * @param hosts the hosts
+     * @return a JSON representation of the hosts
+     */
+    public ArrayNode hosts(Set<UiHost> hosts) {
+        ArrayNode array = arrayNode();
+        for (UiHost host : hosts) {
+            array.add(json(host));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of links.
+     *
+     * @param links the links
+     * @return a JSON representation of the links
+     */
+    public ArrayNode links(Set<UiLink> links) {
+        ArrayNode array = arrayNode();
+        for (UiLink link : links) {
+            array.add(json(link));
+        }
+        return array;
+    }
+
+    // package-private for unit testing
+    List<Set<UiNode>> splitByLayer(List<String> layerTags,
+                                   Set<? extends UiNode> nodes) {
+        final int nLayers = layerTags.size();
+        if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
+            throw new IllegalArgumentException(E_DEF_NOT_LAST);
+        }
+
+        List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
+        Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
+
+        for (String tag : layerTags) {
+            Set<UiNode> set = new HashSet<>();
+            byLayer.put(tag, set);
+            splitList.add(set);
+        }
+
+        for (UiNode n : nodes) {
+            String which = n.layer();
+            if (!layerTags.contains(which)) {
+                which = LAYER_DEFAULT;
+            }
+            byLayer.get(which).add(n);
+        }
+
+        return splitList;
+    }
 }
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 21023fa..7f44244 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
@@ -17,6 +17,7 @@
 package org.onosproject.ui.impl.topo;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import org.onlab.osgi.ServiceDirectory;
 import org.onosproject.ui.RequestHandler;
@@ -24,6 +25,9 @@
 import org.onosproject.ui.UiMessageHandler;
 import org.onosproject.ui.impl.UiWebSocket;
 import org.onosproject.ui.model.topo.UiClusterMember;
+import org.onosproject.ui.model.topo.UiDevice;
+import org.onosproject.ui.model.topo.UiHost;
+import org.onosproject.ui.model.topo.UiLink;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
@@ -31,6 +35,9 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
+
+import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 
 /*
  NOTES:
@@ -58,11 +65,14 @@
     private static final String TOPO2_STOP = "topo2Stop";
 
     // === Outbound event identifiers
+    private static final String ALL_INSTANCES = "topo2AllInstances";
     private static final String CURRENT_LAYOUT = "topo2CurrentLayout";
     private static final String CURRENT_REGION = "topo2CurrentRegion";
-    private static final String ALL_INSTANCES = "topo2AllInstances";
+    private static final String PEER_REGIONS = "topo2PeerRegions";
+    private static final String ORPHANS = "topo2Orphans";
     private static final String TOPO_START_DONE = "topo2StartDone";
 
+
     private UiTopoSession topoSession;
     private Topo2Jsonifier t2json;
 
@@ -99,18 +109,36 @@
 
             log.debug("topo2Start: {}", payload);
 
+            // this is the list of ONOS cluster members
             List<UiClusterMember> instances = topoSession.getAllInstances();
             sendMessage(ALL_INSTANCES, t2json.instances(instances));
 
+            // this is the layout that the user has chosen to display
             UiTopoLayout currentLayout = topoSession.currentLayout();
             sendMessage(CURRENT_LAYOUT, t2json.layout(currentLayout));
 
+            // this is the region that is associated with the current layout
+            //   this message includes details of the sub-regions, devices,
+            //   hosts, and links within the region
+            //   (as well as layer-order hints)
             UiRegion region = topoSession.getRegion(currentLayout);
-            sendMessage(CURRENT_REGION, t2json.region(region));
+            Set<UiRegion> kids = topoSession.getSubRegions(currentLayout);
+            sendMessage(CURRENT_REGION, t2json.region(region, kids));
 
-            // TODO: send information about devices/hosts/links in non-region
-            // TODO: send information about "linked, peer" regions
+            // these are the regions that are siblings to this one
+            Set<UiRegion> peers = topoSession.getPeerRegions(currentLayout);
+            ObjectNode peersPayload = objectNode();
+            peersPayload.set("peers", t2json.closedRegions(peers));
+            sendMessage(PEER_REGIONS, peersPayload);
 
+            // return devices, hosts, links belonging to no region
+            Set<UiDevice> oDevices = topoSession.getOrphanDevices();
+            Set<UiHost> oHosts = topoSession.getOrphanHosts();
+            Set<UiLink> oLinks = topoSession.getOrphanLinks();
+            List<String> oLayers = getOrphanLayerOrder();
+            sendMessage(ORPHANS, t2json.orphans(oDevices, oHosts, oLinks, oLayers));
+
+            // finally, tell the UI that we are done
             sendMessage(TOPO_START_DONE, null);
 
 
@@ -122,6 +150,16 @@
 //            sendAllHosts();
 //            sendTopoStartDone();
         }
+
+
+    }
+
+    // TODO: we need to decide on how this should really get populated.
+    // For example, to be "backward compatible", this should really be
+    //  [ LAYER_OPTICAL, LAYER_PACKET, LAYER_DEFAULT ]
+    private List<String> getOrphanLayerOrder() {
+        // NOTE that LAYER_DEFAULT must always be last in the array
+        return ImmutableList.of(LAYER_DEFAULT);
     }
 
     private final class Topo2Stop extends RequestHandler {
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 1c9fc9d..611a22d 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
@@ -22,12 +22,17 @@
 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.UiDevice;
+import org.onosproject.ui.model.topo.UiHost;
+import org.onosproject.ui.model.topo.UiLink;
 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.Collections;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Coordinates with the {@link UiTopoLayoutService} to access
@@ -44,6 +49,7 @@
  * interact with it when topo-related events come in from the client.
  */
 public class UiTopoSession implements UiModelListener {
+
     private final Logger log = LoggerFactory.getLogger(getClass());
 
     private final UiWebSocket webSocket;
@@ -73,6 +79,13 @@
         this.layoutService = layoutService;
     }
 
+    // constructs a neutered instance, for unit testing
+    UiTopoSession() {
+        webSocket = null;
+        username = null;
+        sharedModel = null;
+    }
+
     /**
      * Initializes the session; registering with the shared model.
      */
@@ -154,6 +167,68 @@
      * @return region that the layout is based upon
      */
     public UiRegion getRegion(UiTopoLayout layout) {
-        return sharedModel.getRegion(layout);
+        return sharedModel.getRegion(layout.regionId());
     }
+
+    /**
+     * 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 are children of the parent layout to this layout.
+     *
+     * @param layout the layout being viewed
+     * @return all regions that are "siblings" to this layout's region
+     */
+    public Set<UiRegion> getPeerRegions(UiTopoLayout layout) {
+        UiRegion currentRegion = getRegion(layout);
+
+        // TODO: consult topo layout service to get hierarchy info...
+        // TODO: then consult shared model to get regions
+        return Collections.emptySet();
+    }
+
+    /**
+     * Returns the subregions of the region in the specified layout.
+     *
+     * @param layout the layout being viewed
+     * @return all regions that are "contained within" this layout's region
+     */
+    public Set<UiRegion> getSubRegions(UiTopoLayout layout) {
+        UiRegion currentRegion = getRegion(layout);
+
+        // TODO: consult topo layout service to get child layouts...
+        // TODO: then consult shared model to get regions
+        return Collections.emptySet();
+    }
+
+
+    /**
+     * Returns all devices that are not in a region.
+     *
+     * @return all devices not in a region
+     */
+    public Set<UiDevice> getOrphanDevices() {
+        // TODO: get devices with no region
+        return Collections.emptySet();
+    }
+
+    /**
+     * Returns all hosts that are not in a region.
+     *
+     * @return all hosts not in a region
+     */
+    public Set<UiHost> getOrphanHosts() {
+        // TODO: get hosts with no region
+        return Collections.emptySet();
+    }
+
+    /**
+     * Returns all links that are not in a region.
+     *
+     * @return all links not in a region
+     */
+    public Set<UiLink> getOrphanLinks() {
+        // TODO: get links with no region
+        return Collections.emptySet();
+    }
+
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
index 54c92f6..debdde9 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/UiSharedTopologyModel.java
@@ -55,6 +55,7 @@
 import org.onosproject.net.link.LinkService;
 import org.onosproject.net.region.Region;
 import org.onosproject.net.region.RegionEvent;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.net.region.RegionListener;
 import org.onosproject.net.region.RegionService;
 import org.onosproject.net.statistic.StatisticService;
@@ -62,15 +63,11 @@
 import org.onosproject.ui.impl.topo.UiTopoSession;
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
-import org.onosproject.ui.model.topo.UiElement;
 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.HashSet;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -210,23 +207,14 @@
         return cache.getAllClusterMembers();
     }
 
-    public Set<UiElement> getElements(UiTopoLayout layout) {
-        Set<UiElement> results = new HashSet<>();
-
-        // TODO: figure out how to extract the appropriate nodes
-        //       from the cache, for the given layout.
-
-        return results;
-    }
-
     /**
-     * Returns the region for the given layout.
+     * Returns the region for the given identifier.
      *
-     * @param layout layout filter
-     * @return the region the layout is based upon
+     * @param id region identifier
+     * @return the region
      */
-    public UiRegion getRegion(UiTopoLayout layout) {
-        return cache.accessRegion(layout.regionId());
+    public UiRegion getRegion(RegionId id) {
+        return cache.accessRegion(id);
     }
 
     // =====================================================================
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
new file mode 100644
index 0000000..1a06c62
--- /dev/null
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/Topo2JsonifierTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+package org.onosproject.ui.impl.topo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+import org.onosproject.ui.impl.AbstractUiImplTest;
+import org.onosproject.ui.model.topo.UiNode;
+
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
+import static org.onosproject.ui.model.topo.UiNode.LAYER_OPTICAL;
+import static org.onosproject.ui.model.topo.UiNode.LAYER_PACKET;
+
+/**
+ * Unit tests for {@link Topo2ViewMessageHandler}.
+ */
+public class Topo2JsonifierTest extends AbstractUiImplTest {
+
+    // mock node class for testing
+    private static class MockNode extends UiNode {
+        private final String id;
+
+        MockNode(String id, String layer) {
+            this.id = id;
+            setLayer(layer);
+        }
+
+        @Override
+        public String idAsString() {
+            return id;
+        }
+
+        @Override
+        public String toString() {
+            return id;
+        }
+    }
+
+    private static final List<String> ALL_TAGS = ImmutableList.of(
+            LAYER_OPTICAL, LAYER_PACKET, LAYER_DEFAULT
+    );
+
+    private static final List<String> PKT_DEF_TAGS = ImmutableList.of(
+            LAYER_PACKET, LAYER_DEFAULT
+    );
+
+    private static final List<String> DEF_TAG_ONLY = ImmutableList.of(
+            LAYER_DEFAULT
+    );
+
+    private static final MockNode NODE_A = new MockNode("A-O", LAYER_OPTICAL);
+    private static final MockNode NODE_B = new MockNode("B-P", LAYER_PACKET);
+    private static final MockNode NODE_C = new MockNode("C-O", LAYER_OPTICAL);
+    private static final MockNode NODE_D = new MockNode("D-D", LAYER_DEFAULT);
+    private static final MockNode NODE_E = new MockNode("E-P", LAYER_PACKET);
+    private static final MockNode NODE_F = new MockNode("F-r", "random");
+
+    private static final Set<MockNode> NODES = ImmutableSet.of(
+            NODE_A, NODE_B, NODE_C, NODE_D, NODE_E, NODE_F
+    );
+
+    private Topo2Jsonifier t2 = new Topo2Jsonifier();
+
+    @Test
+    public void threeLayers() {
+        print("threeLayers()");
+
+        List<Set<UiNode>> result = t2.splitByLayer(ALL_TAGS, NODES);
+        print(result);
+
+        assertEquals("wrong split size", 3, result.size());
+        Set<UiNode> opt = result.get(0);
+        Set<UiNode> pkt = result.get(1);
+        Set<UiNode> def = result.get(2);
+
+        assertEquals("opt bad size", 2, opt.size());
+        assertEquals("missing node A", true, opt.contains(NODE_A));
+        assertEquals("missing node C", true, opt.contains(NODE_C));
+
+        assertEquals("pkt bad size", 2, pkt.size());
+        assertEquals("missing node B", true, pkt.contains(NODE_B));
+        assertEquals("missing node E", true, pkt.contains(NODE_E));
+
+        assertEquals("def bad size", 2, def.size());
+        assertEquals("missing node D", true, def.contains(NODE_D));
+        assertEquals("missing node F", true, def.contains(NODE_F));
+    }
+
+    @Test
+    public void twoLayers() {
+        print("twoLayers()");
+
+        List<Set<UiNode>> result = t2.splitByLayer(PKT_DEF_TAGS, NODES);
+        print(result);
+
+        assertEquals("wrong split size", 2, result.size());
+        Set<UiNode> pkt = result.get(0);
+        Set<UiNode> def = result.get(1);
+
+        assertEquals("pkt bad size", 2, pkt.size());
+        assertEquals("missing node B", true, pkt.contains(NODE_B));
+        assertEquals("missing node E", true, pkt.contains(NODE_E));
+
+        assertEquals("def bad size", 4, def.size());
+        assertEquals("missing node D", true, def.contains(NODE_D));
+        assertEquals("missing node F", true, def.contains(NODE_F));
+        assertEquals("missing node A", true, def.contains(NODE_A));
+        assertEquals("missing node C", true, def.contains(NODE_C));
+    }
+
+    @Test
+    public void oneLayer() {
+        print("oneLayer()");
+
+        List<Set<UiNode>> result = t2.splitByLayer(DEF_TAG_ONLY, NODES);
+        print(result);
+
+        assertEquals("wrong split size", 1, result.size());
+        Set<UiNode> def = result.get(0);
+
+        assertEquals("def bad size", 6, def.size());
+        assertEquals("missing node D", true, def.contains(NODE_D));
+        assertEquals("missing node F", true, def.contains(NODE_F));
+        assertEquals("missing node A", true, def.contains(NODE_A));
+        assertEquals("missing node C", true, def.contains(NODE_C));
+        assertEquals("missing node B", true, def.contains(NODE_B));
+        assertEquals("missing node E", true, def.contains(NODE_E));
+    }
+}