ONOS-4326: Working on topology topo2start processing.
- Added getPeers() to UiTopoLayoutService.
- Fixed wipe-out command to leave the default layout alone.
- Fixed handling of null-region (associated with default layout).
- Added refresh() method to model cache.
- Fixed regions-topo-2 device IDs

Change-Id: Iee49b47ff6702bed9751be7b63392577422d4763
diff --git a/cli/src/main/java/org/onosproject/cli/net/WipeOutCommand.java b/cli/src/main/java/org/onosproject/cli/net/WipeOutCommand.java
index ddd9970..61ea43d 100644
--- a/cli/src/main/java/org/onosproject/cli/net/WipeOutCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/WipeOutCommand.java
@@ -115,7 +115,12 @@
     private void wipeOutLayouts() {
         print("Wiping UI layouts");
         UiTopoLayoutService service = get(UiTopoLayoutService.class);
-        service.getLayouts().forEach(service::removeLayout);
+        // wipe out all layouts except the default, which should always be there
+        service.getLayouts().forEach(l -> {
+            if (!l.id().isDefault()) {
+                service.removeLayout(l);
+            }
+        });
     }
 
     private void wipeOutRegions() {
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 0f2c273..287cc43 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiTopoLayoutService.java
@@ -57,6 +57,15 @@
     UiTopoLayout getLayout(UiTopoLayoutId layoutId);
 
     /**
+     * Returns the set of peer layouts of the specified layout. That is,
+     * those layouts that share the same parent.
+     *
+     * @param layoutId layout identifier
+     * @return set of peer layouts; empty set if layout has no peers
+     */
+    Set<UiTopoLayout> getPeers(UiTopoLayoutId layoutId);
+
+    /**
      * Returns the set of the child layouts of the specified layout.
      *
      * @param layoutId layout identifier
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
index 023300c..c478424 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegion.java
@@ -29,12 +29,27 @@
 import java.util.Set;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.onosproject.net.region.RegionId.regionId;
 
 /**
  * Represents a region.
  */
 public class UiRegion extends UiNode {
 
+    private static final String NULL_NAME = "<null-region>";
+
+    /**
+     * The identifier for the null-region. That is, a container for devices,
+     * hosts, and links for those that belong to no region.
+     */
+    public static final RegionId NULL_ID = regionId(NULL_NAME);
+
+    private static final String[] DEFAULT_LAYER_TAGS = {
+            UiNode.LAYER_OPTICAL,
+            UiNode.LAYER_PACKET,
+            UiNode.LAYER_DEFAULT
+    };
+
     // loose bindings to things in this region
     private final Set<DeviceId> deviceIds = new HashSet<>();
     private final Set<HostId> hostIds = new HashSet<>();
@@ -53,10 +68,12 @@
      * @param region   backing region
      */
     public UiRegion(UiTopology topology, Region region) {
+        // Implementation Note: if region is null, this UiRegion is being used
+        //  as a container for devices, hosts, links that belong to no region.
         this.topology = topology;
         this.region = region;
-        // unless told otherwise, we'll use a single, default layer
-        layerOrder.add(UiNode.LAYER_DEFAULT);
+
+        setLayerOrder(DEFAULT_LAYER_TAGS);
     }
 
     @Override
@@ -83,7 +100,7 @@
      * @return region ID
      */
     public RegionId id() {
-        return region.id();
+        return region == null ? NULL_ID : region.id();
     }
 
     @Override
@@ -93,11 +110,12 @@
 
     @Override
     public String name() {
-        return region.name();
+        return region == null ? NULL_NAME : region.name();
     }
 
     /**
-     * Returns the region instance backing this UI region.
+     * Returns the region instance backing this UI region. If this instance
+     * represents the "null-region", the value returned will be null.
      *
      * @return the backing region instance
      */
@@ -132,7 +150,17 @@
      * @return region type
      */
     public Region.Type type() {
-        return region.type();
+        return region == null ? null : region.type();
+    }
+
+
+    /**
+     * Returns the count of devices in this region.
+     *
+     * @return the device count
+     */
+    public int deviceCount() {
+        return deviceIds.size();
     }
 
     /**
@@ -195,7 +223,7 @@
      * optical layer should be rendered "below" nodes in the packet layer,
      * this method should return:
      * <pre>
-     * [UiNode.LAYER_OPTICAL, UiNode.LAYER_PACKET]
+     * [UiNode.LAYER_OPTICAL, UiNode.LAYER_PACKET, UiNode.LAYER_DEFAULT]
      * </pre>
      *
      * @return layer ordering
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayoutId.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayoutId.java
index 289cacd..3dea4bb 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayoutId.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopoLayoutId.java
@@ -23,10 +23,13 @@
  */
 public final class UiTopoLayoutId extends Identifier<String> {
 
+    private static final String DEFAULT_STR = "_default_";
+
     /**
      * Default topology layout identifier.
      */
-    public static final UiTopoLayoutId DEFAULT_ID = UiTopoLayoutId.layoutId("_default_");
+    public static final UiTopoLayoutId DEFAULT_ID =
+            UiTopoLayoutId.layoutId(DEFAULT_STR);
 
     // For serialization
     private UiTopoLayoutId() {
@@ -45,4 +48,13 @@
     public static UiTopoLayoutId layoutId(String value) {
         return new UiTopoLayoutId(value);
     }
+
+    /**
+     * Returns true if this is the identifier for the default layout.
+     *
+     * @return true if this is the default layout identifier
+     */
+    public boolean isDefault() {
+        return DEFAULT_STR.equals(identifier);
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
index 5ce91da..dd601eb 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiTopology.java
@@ -61,6 +61,9 @@
     private final Map<HostId, UiHost> hostLookup = new HashMap<>();
     private final Map<UiLinkId, UiLink> linkLookup = new HashMap<>();
 
+    // a container for devices, hosts, etc. belonging to no region
+    private final UiRegion nullRegion = new UiRegion(this, null);
+
 
     @Override
     public String toString() {
@@ -89,6 +92,8 @@
         deviceLookup.clear();
         hostLookup.clear();
         linkLookup.clear();
+
+        nullRegion.destroy();
     }
 
 
@@ -145,6 +150,16 @@
     }
 
     /**
+     * Returns a reference to the null-region. That is, the container for
+     * devices, hosts, and links that belong to no region.
+     *
+     * @return the null-region
+     */
+    public UiRegion nullRegion() {
+        return nullRegion;
+    }
+
+    /**
      * Returns the region with the specified identifier, or null if
      * no such region exists.
      *
@@ -186,6 +201,15 @@
     }
 
     /**
+     * Returns all devices in the model.
+     *
+     * @return all devices
+     */
+    public Set<UiDevice> allDevices() {
+        return new HashSet<>(deviceLookup.values());
+    }
+
+    /**
      * Returns the device with the specified identifier, or null if
      * no such device exists.
      *
diff --git a/tools/test/topos/regions-topo-2 b/tools/test/topos/regions-topo-2
index 5a18c91..befed3b 100755
--- a/tools/test/topos/regions-topo-2
+++ b/tools/test/topos/regions-topo-2
@@ -41,18 +41,18 @@
 region-add r3 Region3 CAMPUS ${host}
 
 region-add-devices r1 \
-    of:0000000000000002 \
-    of:0000000000000003 \
-    of:0000000000000004
+    null:0000000000000002 \
+    null:0000000000000003 \
+    null:0000000000000004
 
 region-add-devices r2 \
-    of:0000000000000005 \
-    of:0000000000000006
+    null:0000000000000005 \
+    null:0000000000000006
 
 region-add-devices r3 \
-    of:0000000000000007 \
-    of:0000000000000008 \
-    of:0000000000000009
+    null:0000000000000007 \
+    null:0000000000000008 \
+    null:0000000000000009
 
 regions
 
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 df6dba9..02df84c 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
@@ -181,7 +181,9 @@
             return payload;
         }
         payload.put("id", region.idAsString());
-        payload.set("subregions", jsonSubRegions(subRegions));
+        if (subRegions != null) {
+            payload.set("subregions", jsonSubRegions(subRegions));
+        }
 
         List<String> layerTags = region.layerOrder();
         List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
@@ -226,31 +228,6 @@
         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) {
@@ -270,7 +247,7 @@
                 .put("id", device.idAsString())
                 .put("type", device.type())
                 .put("online", device.isOnline())
-                .put("master", device.master().toString())
+                .put("master", nullIsEmpty(device.master()))
                 .put("layer", device.layer());
 
         // TODO: complete device details
@@ -303,7 +280,8 @@
 
     private ObjectNode jsonClosedRegion(UiRegion region) {
         return objectNode()
-                .put("id", region.idAsString());
+                .put("id", region.idAsString())
+                .put("nDevs", region.deviceCount());
         // TODO: complete closed-region details
     }
 
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 7f44244..5449d93 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,7 +17,6 @@
 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;
@@ -25,9 +24,6 @@
 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;
@@ -37,8 +33,6 @@
 import java.util.List;
 import java.util.Set;
 
-import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
-
 /*
  NOTES:
 
@@ -69,7 +63,6 @@
     private static final String CURRENT_LAYOUT = "topo2CurrentLayout";
     private static final String CURRENT_REGION = "topo2CurrentRegion";
     private static final String PEER_REGIONS = "topo2PeerRegions";
-    private static final String ORPHANS = "topo2Orphans";
     private static final String TOPO_START_DONE = "topo2StartDone";
 
 
@@ -109,6 +102,12 @@
 
             log.debug("topo2Start: {}", payload);
 
+            // this may be a little heavyweight, but it might be safer to do
+            //  this than make assumptions about the order in which devices
+            //  and regions are added... and thus internal linkages set up
+            //  correctly
+            topoSession.refreshModel();
+
             // this is the list of ONOS cluster members
             List<UiClusterMember> instances = topoSession.getAllInstances();
             sendMessage(ALL_INSTANCES, t2json.instances(instances));
@@ -131,14 +130,7 @@
             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
+            // finally, tell the UI that we are done : TODO review / delete??
             sendMessage(TOPO_START_DONE, null);
 
 
@@ -154,14 +146,6 @@
 
     }
 
-    // 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 {
         private Topo2Stop() {
             super(TOPO2_STOP);
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 a597f24..28e345c 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
@@ -34,6 +34,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -110,6 +111,21 @@
     }
 
     @Override
+    public Set<UiTopoLayout> getPeers(UiTopoLayoutId layoutId) {
+        checkNotNull(layoutId, ID_NULL);
+        UiTopoLayout layout = layoutMap.get(layoutId);
+        if (layout == null) {
+            return Collections.emptySet();
+        }
+
+        UiTopoLayoutId parentId = layout.parent();
+        return layoutMap.values().stream()
+                .filter(l -> !Objects.equals(l.id(), layoutId) &&
+                        Objects.equals(l.parent(), parentId))
+                .collect(Collectors.toSet());
+    }
+
+    @Override
     public Set<UiTopoLayout> getChildren(UiTopoLayoutId layoutId) {
         checkNotNull(layoutId, ID_NULL);
         return layoutMap.values().stream()
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 611a22d..47907d4 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
@@ -16,21 +16,19 @@
 
 package org.onosproject.ui.impl.topo;
 
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.UiTopoLayoutService;
 import org.onosproject.ui.impl.UiWebSocket;
 import org.onosproject.ui.impl.topo.model.UiModelEvent;
 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.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -167,23 +165,23 @@
      * @return region that the layout is based upon
      */
     public UiRegion getRegion(UiTopoLayout layout) {
-        return sharedModel.getRegion(layout.regionId());
+        RegionId rid = layout.regionId();
+        return rid == null ? sharedModel.getNullRegion() : sharedModel.getRegion(rid);
     }
 
     /**
      * 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.
+     * layouts that share the same parent layout as 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();
+        Set<UiTopoLayout> peerLayouts = layoutService.getPeers(layout.id());
+        Set<UiRegion> peers = new HashSet<>();
+        peerLayouts.forEach(l -> peers.add(sharedModel.getRegion(l.regionId())));
+        return peers;
     }
 
     /**
@@ -193,42 +191,16 @@
      * @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();
+        Set<UiTopoLayout> kidLayouts = layoutService.getChildren(layout.id());
+        Set<UiRegion> kids = new HashSet<>();
+        kidLayouts.forEach(l -> kids.add(sharedModel.getRegion(l.regionId())));
+        return kids;
     }
 
     /**
-     * Returns all hosts that are not in a region.
-     *
-     * @return all hosts not in a region
+     * Refreshes the model's internal state.
      */
-    public Set<UiHost> getOrphanHosts() {
-        // TODO: get hosts with no region
-        return Collections.emptySet();
+    public void refreshModel() {
+        sharedModel.refresh();
     }
-
-    /**
-     * 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/ModelCache.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
index 8754550..8647c59 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/model/ModelCache.java
@@ -41,6 +41,7 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -169,18 +170,38 @@
         // TODO: post event
     }
 
+    // === THE NULL REGION
+
+    UiRegion nullRegion() {
+        return uiTopology.nullRegion();
+    }
 
     // === REGIONS
 
     private UiRegion addNewRegion(Region r) {
         UiRegion region = new UiRegion(uiTopology, r);
         uiTopology.add(region);
+        log.debug("Region {} added to topology", region);
         return region;
     }
 
     private void updateRegion(UiRegion region) {
-        Set<DeviceId> devs = services.region().getRegionDevices(region.id());
-        region.reconcileDevices(devs);
+        RegionId rid = region.id();
+        Set<DeviceId> deviceIds = services.region().getRegionDevices(rid);
+
+        // Make sure device objects refer to their region
+        deviceIds.forEach(d -> {
+            UiDevice dev = uiTopology.findDevice(d);
+            if (dev != null) {
+                dev.setRegionId(rid);
+            } else {
+                // if we don't have the UiDevice in the topology, what can we do?
+                log.warn("Region device {}, but we don't have UiDevice in topology", d);
+            }
+        });
+
+        // Make sure the region object refers to the devices
+        region.reconcileDevices(deviceIds);
     }
 
     private void loadRegions() {
@@ -224,21 +245,22 @@
 
     private UiDevice addNewDevice(Device d) {
         UiDevice device = new UiDevice(uiTopology, d);
+        updateDevice(device);
         uiTopology.add(device);
+        log.debug("Device {} added to topology", device);
         return device;
     }
 
+    // make sure the UiDevice is tagged with the region it belongs to
     private void updateDevice(UiDevice device) {
-        Region regionForDevice = services.region().getRegionForDevice(device.id());
-        if (regionForDevice != null) {
-            device.setRegionId(regionForDevice.id());
-        }
+        Region r = services.region().getRegionForDevice(device.id());
+        RegionId rid = r == null ? UiRegion.NULL_ID : r.id();
+        device.setRegionId(rid);
     }
 
     private void loadDevices() {
         for (Device d : services.device().getDevices()) {
-            UiDevice device = addNewDevice(d);
-            updateDevice(device);
+            addNewDevice(d);
         }
     }
 
@@ -248,8 +270,9 @@
         UiDevice uiDevice = uiTopology.findDevice(id);
         if (uiDevice == null) {
             uiDevice = addNewDevice(device);
+        } else {
+            updateDevice(uiDevice);
         }
-        updateDevice(uiDevice);
 
         postEvent(DEVICE_ADDED_OR_UPDATED, uiDevice);
     }
@@ -434,6 +457,43 @@
     }
 
 
+    /**
+     * Refreshes the internal state.
+     */
+    public void refresh() {
+        // fix up internal linkages if they aren't correct
+
+        // at the moment, this is making sure devices are in the correct region
+        Set<UiDevice> allDevices = uiTopology.allDevices();
+
+        services.region().getRegions().forEach(r -> {
+            RegionId rid = r.id();
+            UiRegion region = uiTopology.findRegion(rid);
+            if (region != null) {
+                Set<DeviceId> deviceIds = services.region().getRegionDevices(rid);
+                region.reconcileDevices(deviceIds);
+
+                deviceIds.forEach(devId -> {
+                    UiDevice dev = uiTopology.findDevice(devId);
+                    if (dev != null) {
+                        dev.setRegionId(r.id());
+                        allDevices.remove(dev);
+                    } else {
+                        log.warn("Region device ID {} but no UiDevice in topology",
+                                devId);
+                    }
+                });
+            } else {
+                log.warn("No UiRegion in topology for ID {}", rid);
+            }
+        });
+
+        // what is left over, must belong to the null-region
+        Set<DeviceId> leftOver = new HashSet<>(allDevices.size());
+        allDevices.forEach(d -> leftOver.add(d.id()));
+        uiTopology.nullRegion().reconcileDevices(leftOver);
+    }
+
     // === CACHE STATISTICS
 
     /**
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 50ebb06..4fbf707 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
@@ -217,6 +217,22 @@
         return cache.accessRegion(id);
     }
 
+    /**
+     * Returns the null region.
+     *
+     * @return the null region
+     */
+    public UiRegion getNullRegion() {
+        return cache.nullRegion();
+    }
+
+    /**
+     * Refreshes the cache's internal state.
+     */
+    public void refresh() {
+        cache.refresh();
+    }
+
     // =====================================================================
 
 
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 938d1af..dcf5086 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
@@ -1,6 +1,31 @@
 {
   "event": "topo2CurrentRegion",
   "payload": {
-    "note": "no-region"
+    "id": "<null-region>",
+    "subregions": [{
+      "id": "r2",
+      "nDevs": 2
+    }, {
+      "id": "r1",
+      "nDevs": 3
+    }],
+    "devices": [
+      [],
+      [],
+      [{
+        "id": "null:0000000000000001",
+        "type": "switch",
+        "online": false,
+        "master": "",
+        "layer": "def"
+      }]
+    ],
+    "hosts": [
+      [],
+      [],
+      []
+    ],
+    "links": [],
+    "layerOrder": ["opt", "pkt", "def"]
   }
 }
diff --git a/web/gui/src/test/_karma/ev/topo2a/ev_5_orphans.json b/web/gui/src/test/_karma/ev/topo2a/ev_5_orphans.json
deleted file mode 100644
index 751127d..0000000
--- a/web/gui/src/test/_karma/ev/topo2a/ev_5_orphans.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "event": "topo2Orphans",
-  "payload": {
-    "devices": [
-      []
-    ],
-    "hosts": [
-      []
-    ],
-    "links": [],
-    "layerOrder": ["def"]
-  }
-}
diff --git a/web/gui/src/test/_karma/ev/topo2a/ev_6_startDone.json b/web/gui/src/test/_karma/ev/topo2a/ev_5_startDone.json
similarity index 100%
rename from web/gui/src/test/_karma/ev/topo2a/ev_6_startDone.json
rename to web/gui/src/test/_karma/ev/topo2a/ev_5_startDone.json