ONOS-4971: Synthetic Link Data -- WIP

- Breaking out UiLink to subclasses for device links, host links, region links, region-device links,
    - (soon, also peer links).
- Augmenting UiLinkId to include regions as endpoints.
- Introduced UiSynthLink to encapsulate synthetic links bound to regions.
- Model Cache now computes synthetic links from the underlying link data.
- Added endPointA/B() and type() methods to UiLink.
- Updated topo2CurrentRegion response to include synth-links for the region.

Change-Id: Ifa62a15fbe0a58b134d92278b201fa7a72cbfa83
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java
new file mode 100644
index 0000000..4575678
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiDeviceLink.java
@@ -0,0 +1,180 @@
+/*
+ * 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.model.topo;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Represents a link between two devices; that is, an infrastructure link.
+ */
+public class UiDeviceLink extends UiLink {
+
+    private static final String E_UNASSOC =
+            "backing link not associated with this UI device link: ";
+
+    // devices and ports at either end of this link
+    private DeviceId deviceA;
+    private DeviceId deviceB;
+    private PortNumber portA;
+    private PortNumber portB;
+
+    // two unidirectional links underlying this link...
+    private Link linkAtoB;
+    private Link linkBtoA;
+
+
+    /**
+     * Creates a device to device UI link.
+     *
+     * @param topology parent topology
+     * @param id       canonicalized link identifier
+     */
+    public UiDeviceLink(UiTopology topology, UiLinkId id) {
+        super(topology, id);
+    }
+
+    @Override
+    public String endPointA() {
+        return deviceA + UiLinkId.ID_PORT_DELIMITER + portA;
+    }
+
+    @Override
+    public String endPointB() {
+        return deviceB + UiLinkId.ID_PORT_DELIMITER + portB;
+    }
+
+
+    @Override
+    protected void destroy() {
+        deviceA = null;
+        deviceB = null;
+        portA = null;
+        portB = null;
+        linkAtoB = null;
+        linkBtoA = null;
+    }
+
+
+    /**
+     * Attaches the given backing link to this UI link. This method will
+     * throw an exception if this UI link is not representative of the
+     * supplied link.
+     *
+     * @param link backing link to attach
+     * @throws IllegalArgumentException if the link is not appropriate
+     */
+    public void attachBackingLink(Link link) {
+        UiLinkId.Direction d = id.directionOf(link);
+
+        if (d == UiLinkId.Direction.A_TO_B) {
+            linkAtoB = link;
+            deviceA = link.src().deviceId();
+            portA = link.src().port();
+            deviceB = link.dst().deviceId();
+            portB = link.dst().port();
+
+        } else if (d == UiLinkId.Direction.B_TO_A) {
+            linkBtoA = link;
+            deviceB = link.src().deviceId();
+            portB = link.src().port();
+            deviceA = link.dst().deviceId();
+            portA = link.dst().port();
+
+        } else {
+            throw new IllegalArgumentException(E_UNASSOC + link);
+        }
+    }
+
+    /**
+     * Detaches the given backing link from this UI link, returning true if the
+     * reverse link is still attached, or false otherwise.
+     *
+     * @param link the backing link to detach
+     * @return true if other link still attached, false otherwise
+     * @throws IllegalArgumentException if the link is not appropriate
+     */
+    public boolean detachBackingLink(Link link) {
+        UiLinkId.Direction d = id.directionOf(link);
+        if (d == UiLinkId.Direction.A_TO_B) {
+            linkAtoB = null;
+            return linkBtoA != null;
+        }
+        if (d == UiLinkId.Direction.B_TO_A) {
+            linkBtoA = null;
+            return linkAtoB != null;
+        }
+        throw new IllegalArgumentException(E_UNASSOC + link);
+    }
+
+
+    /**
+     * Returns the identity of device A.
+     *
+     * @return device A ID
+     */
+    public DeviceId deviceA() {
+        return deviceA;
+    }
+
+    /**
+     * Returns the port number of device A.
+     *
+     * @return port A
+     */
+    public PortNumber portA() {
+        return portA;
+    }
+
+    /**
+     * Returns the identity of device B.
+     *
+     * @return device B ID
+     */
+    public DeviceId deviceB() {
+        return deviceB;
+    }
+
+    /**
+     * Returns the port number of device B.
+     *
+     * @return port B
+     */
+    public PortNumber portB() {
+        return portB;
+    }
+
+    /**
+     * Returns backing link from A to B.
+     *
+     * @return backing link A to B
+     */
+    public Link linkAtoB() {
+        return linkAtoB;
+    }
+
+    /**
+     * Returns backing link from B to A.
+     *
+     * @return backing link B to A
+     */
+    public Link linkBtoA() {
+        return linkBtoA;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiEdgeLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiEdgeLink.java
new file mode 100644
index 0000000..909ef1a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiEdgeLink.java
@@ -0,0 +1,84 @@
+/*
+ * 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.model.topo;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.EdgeLink;
+import org.onosproject.net.PortNumber;
+
+/**
+ * Designates a link between a device and a host; that is, an edge link.
+ */
+public class UiEdgeLink extends UiLink {
+
+    private static final String E_UNASSOC =
+            "backing link not associated with this UI edge link: ";
+
+    // private (synthetic) host link
+    private DeviceId edgeDevice;
+    private PortNumber edgePort;
+    private EdgeLink edgeLink;
+
+    /**
+     * Creates a UI link.
+     *
+     * @param topology parent topology
+     * @param id       canonicalized link identifier
+     */
+    public UiEdgeLink(UiTopology topology, UiLinkId id) {
+        super(topology, id);
+    }
+
+    @Override
+    public String endPointA() {
+        return edgeLink.hostId().toString();
+    }
+
+    @Override
+    public String endPointB() {
+        return edgeDevice + UiLinkId.ID_PORT_DELIMITER + edgePort;
+    }
+
+    @Override
+    protected void destroy() {
+        edgeDevice = null;
+        edgePort = null;
+        edgeLink = null;
+    }
+
+    /**
+     * Attaches the given edge link to this UI link. This method will
+     * throw an exception if this UI link is not representative of the
+     * supplied link.
+     *
+     * @param elink edge link to attach
+     * @throws IllegalArgumentException if the link is not appropriate
+     */
+    public void attachEdgeLink(EdgeLink elink) {
+        UiLinkId.Direction d = id.directionOf(elink);
+        // Expected direction of edge links is A-to-B (Host to device)
+        // but checking not null is a sufficient test
+        if (d == null) {
+            throw new IllegalArgumentException(E_UNASSOC + elink);
+        }
+
+        edgeLink = elink;
+        edgeDevice = elink.hostLocation().deviceId();
+        edgePort = elink.hostLocation().port();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
index 916944c..0f65e9f 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLink.java
@@ -16,17 +16,11 @@
 
 package org.onosproject.ui.model.topo;
 
-import org.onosproject.net.DeviceId;
-import org.onosproject.net.EdgeLink;
-import org.onosproject.net.Link;
-
-import java.util.Set;
-
 import static com.google.common.base.MoreObjects.toStringHelper;
 
 /**
- * Represents a link (line between two elements). This may have one of
- * several forms:
+ * Represents a link (line between two elements). This may be one of
+ * several concrete subclasses:
  * <ul>
  * <li>
  * An infrastructure link:
@@ -38,17 +32,15 @@
  * </li>
  * <li>
  * An aggregation link:
- * representing multiple underlying UI link instances.
+ * representing multiple underlying UI link instances, for example
+ * the link between two sub-regions in a region (layout).
  * </li>
  * </ul>
  */
-public class UiLink extends UiElement {
+public abstract class UiLink extends UiElement {
 
-    private static final String E_UNASSOC =
-            "backing link not associated with this UI link: ";
-
-    private final UiTopology topology;
-    private final UiLinkId id;
+    protected final UiTopology topology;
+    protected final UiLinkId id;
 
     /**
      * Creates a UI link.
@@ -61,22 +53,6 @@
         this.id = id;
     }
 
-    // devices at either end of this link
-    private DeviceId deviceA;
-    private DeviceId deviceB;
-
-    // two unidirectional links underlying this link...
-    private Link linkAtoB;
-    private Link linkBtoA;
-
-    // ==OR== : private (synthetic) host link
-    private DeviceId edgeDevice;
-    private EdgeLink edgeLink;
-
-    // ==OR== : set of underlying UI links that this link aggregates
-    private Set<UiLink> children;
-
-
     @Override
     public String toString() {
         return toStringHelper(this)
@@ -84,19 +60,6 @@
                 .toString();
     }
 
-    @Override
-    protected void destroy() {
-        deviceA = null;
-        deviceB = null;
-        linkAtoB = null;
-        linkBtoA = null;
-        edgeLink = null;
-        if (children != null) {
-            children.clear();
-            children = null;
-        }
-    }
-
     /**
      * Returns the canonicalized link identifier for this link.
      *
@@ -112,107 +75,25 @@
     }
 
     /**
-     * Attaches the given backing link to this UI link. This method will
-     * throw an exception if this UI link is not representative of the
-     * supplied link.
+     * Returns the implementing class name as the type of link.
      *
-     * @param link backing link to attach
-     * @throws IllegalArgumentException if the link is not appropriate
+     * @return link type
      */
-    public void attachBackingLink(Link link) {
-        UiLinkId.Direction d = id.directionOf(link);
-
-        if (d == UiLinkId.Direction.A_TO_B) {
-            linkAtoB = link;
-            deviceA = link.src().deviceId();
-            deviceB = link.dst().deviceId();
-
-        } else if (d == UiLinkId.Direction.B_TO_A) {
-            linkBtoA = link;
-            deviceB = link.src().deviceId();
-            deviceA = link.dst().deviceId();
-
-        } else {
-            throw new IllegalArgumentException(E_UNASSOC + link);
-        }
+    public String type() {
+        return getClass().getSimpleName();
     }
 
     /**
-     * Detaches the given backing link from this UI link, returning true if the
-     * reverse link is still attached, or false otherwise.
+     * Returns the identifier of end-point A in string form.
      *
-     * @param link the backing link to detach
-     * @return true if other link still attached, false otherwise
-     * @throws IllegalArgumentException if the link is not appropriate
+     * @return end point A identifier
      */
-    public boolean detachBackingLink(Link link) {
-        UiLinkId.Direction d = id.directionOf(link);
-        if (d == UiLinkId.Direction.A_TO_B) {
-            linkAtoB = null;
-            return linkBtoA != null;
-        }
-        if (d == UiLinkId.Direction.B_TO_A) {
-            linkBtoA = null;
-            return linkAtoB != null;
-        }
-        throw new IllegalArgumentException(E_UNASSOC + link);
-    }
+    public abstract String endPointA();
 
     /**
-     * Attaches the given edge link to this UI link. This method will
-     * throw an exception if this UI link is not representative of the
-     * supplied link.
+     * Returns the identifier of end-point B in string form.
      *
-     * @param elink edge link to attach
-     * @throws IllegalArgumentException if the link is not appropriate
+     * @return end point B identifier
      */
-    public void attachEdgeLink(EdgeLink elink) {
-        UiLinkId.Direction d = id.directionOf(elink);
-        // Expected direction of edge links is A-to-B (Host to device)
-        // but checking not null is sufficient
-        if (d == null) {
-            throw new IllegalArgumentException(E_UNASSOC + elink);
-        }
-
-        edgeLink = elink;
-        edgeDevice = elink.hostLocation().deviceId();
-    }
-
-
-    /**
-     * Returns the identity of device A.
-     *
-     * @return device A ID
-     */
-    public DeviceId deviceA() {
-        return deviceA;
-    }
-
-    /**
-     * Returns the identity of device B.
-     *
-     * @return device B ID
-     */
-    public DeviceId deviceB() {
-        return deviceB;
-    }
-
-    /**
-     * Returns backing link from A to B.
-     *
-     * @return backing link A to B
-     */
-    public Link linkAtoB() {
-        return linkAtoB;
-    }
-
-    /**
-     * Returns backing link from B to A.
-     *
-     * @return backing link B to A
-     */
-    public Link linkBtoA() {
-        return linkBtoA;
-    }
-
+    public abstract String endPointB();
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
index e530db4..3bde82a 100644
--- a/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiLinkId.java
@@ -17,15 +17,30 @@
 package org.onosproject.ui.model.topo;
 
 import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.ElementId;
 import org.onosproject.net.Link;
 import org.onosproject.net.PortNumber;
+import org.onosproject.net.region.RegionId;
+
+import java.util.Comparator;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
  * A canonical representation of an identifier for {@link UiLink}s.
  */
 public final class UiLinkId {
 
+    private static final String E_PORT_NULL = "Port number cannot be null";
+    private static final String E_DEVICE_ID_NULL = "Device ID cannot be null";
+    private static final String E_REGION_ID_NULL = "Region ID cannot be null";
+    private static final String E_IDENTICAL = "Region IDs cannot be same";
+
+    private static final Comparator<RegionId> REGION_ID_COMPARATOR =
+            (o1, o2) -> o1.toString().compareTo(o2.toString());
+
     /**
      * Designates the directionality of an underlying (uni-directional) link.
      */
@@ -34,12 +49,15 @@
         B_TO_A
     }
 
-    private static final String CP_DELIMITER = "~";
-    private static final String ID_PORT_DELIMITER = "/";
+    static final String CP_DELIMITER = "~";
+    static final String ID_PORT_DELIMITER = "/";
 
-    private final ElementId idA;
+    private final RegionId regionA;
+    private final ElementId elementA;
     private final PortNumber portA;
-    private final ElementId idB;
+
+    private final RegionId regionB;
+    private final ElementId elementB;
     private final PortNumber portB;
 
     private final String idStr;
@@ -50,37 +68,81 @@
      * which is invariant to whether A or B is source or destination of the
      * underlying link.
      *
-     * @param a first element ID
+     * @param a  first element ID
      * @param pa first element port
-     * @param b second element ID
+     * @param b  second element ID
      * @param pb second element port
      */
     private UiLinkId(ElementId a, PortNumber pa, ElementId b, PortNumber pb) {
-        idA = a;
+        elementA = a;
         portA = pa;
-        idB = b;
+        elementB = b;
         portB = pb;
 
+        regionA = null;
+        regionB = null;
+
         idStr = a + ID_PORT_DELIMITER + pa + CP_DELIMITER +
                 b + ID_PORT_DELIMITER + pb;
     }
 
+    /**
+     * Creates a UI link identifier. It is expected that A comes before B when
+     * the two identifiers are naturally sorted.
+     *
+     * @param a first region ID
+     * @param b second region ID
+     */
+    private UiLinkId(RegionId a, RegionId b) {
+        regionA = a;
+        regionB = b;
+
+        elementA = null;
+        elementB = null;
+        portA = null;
+        portB = null;
+
+        idStr = a + CP_DELIMITER + b;
+    }
+
+    /**
+     * Creates a UI link identifier, with region at one end and a device/port
+     * at the other.
+     *
+     * @param r region ID
+     * @param d device ID
+     * @param p port number
+     */
+    private UiLinkId(RegionId r, DeviceId d, PortNumber p) {
+        regionA = r;
+        elementB = d;
+        portB = p;
+
+        regionB = null;
+        elementA = null;
+        portA = null;
+
+        idStr = r + CP_DELIMITER + elementB + ID_PORT_DELIMITER + portB;
+    }
+
     @Override
     public String toString() {
         return idStr;
     }
 
     /**
-     * Returns the identifier of the first element.
+     * Returns the identifier of the first element. Note that the returned
+     * value will be null if this identifier is for a region-region link.
      *
      * @return first element identity
      */
     public ElementId elementA() {
-        return idA;
+        return elementA;
     }
 
     /**
-     * Returns the port of the first element.
+     * Returns the port of the first element. Note that the returned
+     * value will be null if this identifier is for a region-region link.
      *
      * @return first element port
      */
@@ -89,16 +151,18 @@
     }
 
     /**
-     * Returns the identifier of the second element.
+     * Returns the identifier of the second element. Note that the returned
+     * value will be null if this identifier is for a region-region link.
      *
      * @return second element identity
      */
     public ElementId elementB() {
-        return idB;
+        return elementB;
     }
 
     /**
-     * Returns the port of the second element.
+     * Returns the port of the second element. Note that the returned
+     * value will be null if this identifier is for a region-region link.
      *
      * @return second element port
      */
@@ -106,6 +170,28 @@
         return portB;
     }
 
+    /**
+     * Returns the identity of the first region. Note that the returned value
+     * will be null if this identifier is for a device-device or device-host
+     * link.
+     *
+     * @return first region ID
+     */
+    public RegionId regionA() {
+        return regionA;
+    }
+
+    /**
+     * Returns the identity of the second region. Note that the returned value
+     * will be null if this identifier is for a device-device or device-host
+     * link.
+     *
+     * @return second region ID
+     */
+    public RegionId regionB() {
+        return regionB;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) {
@@ -134,8 +220,8 @@
     Direction directionOf(Link link) {
         ConnectPoint src = link.src();
         ElementId srcId = src.elementId();
-        return idA.equals(srcId) ? Direction.A_TO_B
-                : idB.equals(srcId) ? Direction.B_TO_A
+        return elementA.equals(srcId) ? Direction.A_TO_B
+                : elementB.equals(srcId) ? Direction.B_TO_A
                 : null;
     }
 
@@ -161,4 +247,42 @@
         return comp <= 0 ? new UiLinkId(srcId, src.port(), dstId, dst.port())
                 : new UiLinkId(dstId, dst.port(), srcId, src.port());
     }
+
+    /**
+     * Generates the canonical link identifier for a link between the
+     * specified region nodes.
+     *
+     * @param one the first region ID
+     * @param two the second region ID
+     * @return link identifier
+     * @throws NullPointerException     if any of the required fields are null
+     * @throws IllegalArgumentException if the identifiers are identical
+     */
+    public static UiLinkId uiLinkId(RegionId one, RegionId two) {
+        checkNotNull(one, E_REGION_ID_NULL);
+        checkNotNull(two, E_REGION_ID_NULL);
+        checkArgument(!one.equals(two), E_IDENTICAL);
+
+        boolean flip = REGION_ID_COMPARATOR.compare(one, two) > 0;
+        return flip ? new UiLinkId(two, one) : new UiLinkId(one, two);
+    }
+
+    /**
+     * Generates the canonical link identifier for a link between the specified
+     * region and device/port.
+     *
+     * @param regionId   region ID
+     * @param deviceId   device ID
+     * @param portNumber port number
+     * @return link identifier
+     * @throws NullPointerException if any of the required fields are null
+     */
+    public static UiLinkId uiLinkId(RegionId regionId, DeviceId deviceId,
+                                    PortNumber portNumber) {
+        checkNotNull(regionId, E_REGION_ID_NULL);
+        checkNotNull(deviceId, E_DEVICE_ID_NULL);
+        checkNotNull(portNumber, E_PORT_NULL);
+
+        return new UiLinkId(regionId, deviceId, portNumber);
+    }
 }
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 b3185f1..87d5036 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
@@ -53,7 +53,6 @@
     // loose bindings to things in this region
     private final Set<DeviceId> deviceIds = new HashSet<>();
     private final Set<HostId> hostIds = new HashSet<>();
-    private final Set<UiLinkId> uiLinkIds = new HashSet<>();
 
     private final List<String> layerOrder = new ArrayList<>();
 
@@ -84,7 +83,6 @@
     protected void destroy() {
         deviceIds.clear();
         hostIds.clear();
-        uiLinkIds.clear();
     }
 
     /**
@@ -135,6 +133,15 @@
     }
 
     /**
+     * Returns the UI region that is the parent of this region.
+     *
+     * @return the parent region
+     */
+    public UiRegion parentRegion() {
+        return topology.findRegion(parent);
+    }
+
+    /**
      * Sets the parent ID for this region.
      *
      * @param parentId parent ID
@@ -192,7 +199,6 @@
                 .add("kids", kids)
                 .add("devices", deviceIds)
                 .add("#hosts", hostIds.size())
-                .add("#links", uiLinkIds.size())
                 .toString();
     }
 
@@ -252,24 +258,6 @@
     }
 
     /**
-     * Returns the set of link identifiers for this region.
-     *
-     * @return link identifiers for this region
-     */
-    public Set<UiLinkId> linkIds() {
-        return ImmutableSet.copyOf(uiLinkIds);
-    }
-
-    /**
-     * Returns the links in this region.
-     *
-     * @return the links in this region
-     */
-    public Set<UiLink> links() {
-        return topology.linkSet(uiLinkIds);
-    }
-
-    /**
      * Returns the order in which layers should be rendered. Lower layers
      * come earlier in the list. For example, to indicate that nodes in the
      * optical layer should be rendered "below" nodes in the packet layer,
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionDeviceLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionDeviceLink.java
new file mode 100644
index 0000000..f749473
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionDeviceLink.java
@@ -0,0 +1,92 @@
+/*
+ * 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.model.topo;
+
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.region.RegionId;
+
+
+/**
+ * Designates a link between a region node and a device.
+ */
+public class UiRegionDeviceLink extends UiLink {
+
+    private static final String E_NOT_REGION_DEVICE_ID =
+            "UI link identifier not region to device";
+
+    // private (synthetic) region-device link
+    private final RegionId region;
+    private final DeviceId device;
+    private final PortNumber port;
+
+    /**
+     * Creates a region to device UI link. Note that it is expected that the
+     * link identifier is one that has a region ID at one end, and a device
+     * ID at the other
+     *
+     * @param topology parent topology
+     * @param id       canonicalized link identifier
+     * @throws IllegalArgumentException if the link ID is not region-region
+     */
+    public UiRegionDeviceLink(UiTopology topology, UiLinkId id) {
+        super(topology, id);
+        region = id.regionA();
+        device = (DeviceId) id.elementB();
+        port = id.portB();
+        if (region == null || device == null || port == null) {
+            throw new IllegalArgumentException(E_NOT_REGION_DEVICE_ID);
+        }
+    }
+
+    @Override
+    public String endPointA() {
+        return region.id();
+    }
+
+    @Override
+    public String endPointB() {
+        return device + UiLinkId.ID_PORT_DELIMITER + port;
+    }
+
+    /**
+     * Returns the identity of the region.
+     *
+     * @return region ID
+     */
+    public RegionId region() {
+        return region;
+    }
+
+    /**
+     * Returns the identity of the device.
+     *
+     * @return device ID
+     */
+    public DeviceId device() {
+        return device;
+    }
+
+    /**
+     * Returns the identity of the device port.
+     *
+     * @return device port number
+     */
+    public PortNumber port() {
+        return port;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionLink.java
new file mode 100644
index 0000000..7c3456c
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiRegionLink.java
@@ -0,0 +1,78 @@
+/*
+ * 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.model.topo;
+
+import org.onosproject.net.region.RegionId;
+
+
+/**
+ * Designates a link between two region nodes.
+ */
+public class UiRegionLink extends UiLink {
+
+    private static final String E_NOT_REGION_ID =
+            "UI link identifier not region to region";
+
+    // private (synthetic) region - region link
+    private final RegionId regionA;
+    private final RegionId regionB;
+
+    /**
+     * Creates a region to region UI link. Note that it is expected that the
+     * link identifier is one that has region IDs as source and destination.
+     *
+     * @param topology parent topology
+     * @param id       canonicalized link identifier
+     * @throws IllegalArgumentException if the link ID is not region-region
+     */
+    public UiRegionLink(UiTopology topology, UiLinkId id) {
+        super(topology, id);
+        regionA = id.regionA();
+        regionB = id.regionB();
+        if (regionA == null || regionB == null) {
+            throw new IllegalArgumentException(E_NOT_REGION_ID);
+        }
+    }
+
+    @Override
+    public String endPointA() {
+        return regionA.id();
+    }
+
+    @Override
+    public String endPointB() {
+        return regionB.id();
+    }
+
+    /**
+     * Returns the identity of the first region.
+     *
+     * @return first region ID
+     */
+    public RegionId regionA() {
+        return regionA;
+    }
+
+    /**
+     * Returns the identity of the second region.
+     *
+     * @return second region ID
+     */
+    public RegionId regionB() {
+        return regionB;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java b/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java
new file mode 100644
index 0000000..7e67430
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/model/topo/UiSynthLink.java
@@ -0,0 +1,68 @@
+/*
+ * 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.model.topo;
+
+import org.onosproject.net.region.RegionId;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * A synthetic link that encapsulates a UiLink instance and the region to
+ * which it belongs.
+ */
+public class UiSynthLink {
+
+    private final RegionId regionId;
+    private final UiLink link;
+
+    /**
+     * Constructs a synthetic link with the given parameters.
+     *
+     * @param regionId the region to which the link belongs
+     * @param link     the link instance
+     */
+    public UiSynthLink(RegionId regionId, UiLink link) {
+        this.regionId = regionId;
+        this.link = link;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("region", regionId)
+                .add("link", link)
+                .toString();
+    }
+
+    /**
+     * Returns the region identifier.
+     *
+     * @return the region ID
+     */
+    public RegionId regionId() {
+        return regionId;
+    }
+
+    /**
+     * Returns the link.
+     *
+     * @return the link
+     */
+    public UiLink link() {
+        return link;
+    }
+}
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 708fd18..3297608 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
@@ -19,6 +19,7 @@
 import org.onosproject.cluster.NodeId;
 import org.onosproject.net.DeviceId;
 import org.onosproject.net.HostId;
+import org.onosproject.net.PortNumber;
 import org.onosproject.net.region.RegionId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -30,9 +31,12 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.onosproject.ui.model.topo.UiLinkId.uiLinkId;
 
 /**
  * Represents the overall network topology.
@@ -59,7 +63,11 @@
     private final Map<RegionId, UiRegion> regionLookup = new HashMap<>();
     private final Map<DeviceId, UiDevice> deviceLookup = new HashMap<>();
     private final Map<HostId, UiHost> hostLookup = new HashMap<>();
-    private final Map<UiLinkId, UiLink> linkLookup = new HashMap<>();
+    private final Map<UiLinkId, UiDeviceLink> devLinkLookup = new HashMap<>();
+    private final Map<UiLinkId, UiEdgeLink> edgeLinkLookup = new HashMap<>();
+
+    // a cache of the computed synthetic links
+    private final List<UiSynthLink> synthLinks = new ArrayList<>();
 
     // a container for devices, hosts, etc. belonging to no region
     private final UiRegion nullRegion = new UiRegion(this, null);
@@ -72,7 +80,9 @@
                 .add("#regions", regionCount())
                 .add("#devices", deviceLookup.size())
                 .add("#hosts", hostLookup.size())
-                .add("#links", linkLookup.size())
+                .add("#dev-links", devLinkLookup.size())
+                .add("#edge-links", edgeLinkLookup.size())
+                .add("#synth-links", synthLinks.size())
                 .toString();
     }
 
@@ -91,7 +101,10 @@
         regionLookup.clear();
         deviceLookup.clear();
         hostLookup.clear();
-        linkLookup.clear();
+        devLinkLookup.clear();
+        edgeLinkLookup.clear();
+
+        synthLinks.clear();
 
         nullRegion.destroy();
     }
@@ -261,54 +274,96 @@
         return deviceLookup.size();
     }
 
+
     /**
-     * Returns all links in the model.
+     * Returns all device links in the model.
      *
-     * @return all links
+     * @return all device links
      */
-    public Set<UiLink> allLinks() {
-        return new HashSet<>(linkLookup.values());
+    public Set<UiDeviceLink> allDeviceLinks() {
+        return new HashSet<>(devLinkLookup.values());
     }
 
     /**
-     * Returns the link with the specified identifier, or null if no such
-     * link exists.
+     * Returns the device link with the specified identifier, or null if no
+     * such link exists.
      *
      * @param id the canonicalized link identifier
-     * @return corresponding UI link
+     * @return corresponding UI device link
      */
-    public UiLink findLink(UiLinkId id) {
-        return linkLookup.get(id);
+    public UiDeviceLink findDeviceLink(UiLinkId id) {
+        return devLinkLookup.get(id);
     }
 
     /**
-     * Adds the given UI link to the topology model.
+     * Returns the edge link with the specified identifier, or null if no
+     * such link exists.
      *
-     * @param uiLink link to add
+     * @param id the canonicalized link identifier
+     * @return corresponding UI edge link
      */
-    public void add(UiLink uiLink) {
-        linkLookup.put(uiLink.id(), uiLink);
+    public UiEdgeLink findEdgeLink(UiLinkId id) {
+        return edgeLinkLookup.get(id);
     }
 
     /**
-     * Removes the given UI link from the model.
+     * Adds the given UI device link to the topology model.
      *
-     * @param uiLink link to remove
+     * @param uiDeviceLink link to add
      */
-    public void remove(UiLink uiLink) {
-        UiLink link = linkLookup.remove(uiLink.id());
+    public void add(UiDeviceLink uiDeviceLink) {
+        devLinkLookup.put(uiDeviceLink.id(), uiDeviceLink);
+    }
+
+    /**
+     * Adds the given UI edge link to the topology model.
+     *
+     * @param uiEdgeLink link to add
+     */
+    public void add(UiEdgeLink uiEdgeLink) {
+        edgeLinkLookup.put(uiEdgeLink.id(), uiEdgeLink);
+    }
+
+    /**
+     * Removes the given UI device link from the model.
+     *
+     * @param uiDeviceLink link to remove
+     */
+    public void remove(UiDeviceLink uiDeviceLink) {
+        UiDeviceLink link = devLinkLookup.remove(uiDeviceLink.id());
         if (link != null) {
             link.destroy();
         }
     }
 
     /**
-     * Returns the number of links configured in the topology.
+     * Removes the given UI edge link from the model.
      *
-     * @return number of links
+     * @param uiEdgeLink link to remove
      */
-    public int linkCount() {
-        return linkLookup.size();
+    public void remove(UiEdgeLink uiEdgeLink) {
+        UiEdgeLink link = edgeLinkLookup.remove(uiEdgeLink.id());
+        if (link != null) {
+            link.destroy();
+        }
+    }
+
+    /**
+     * Returns the number of device links configured in the topology.
+     *
+     * @return number of device links
+     */
+    public int deviceLinkCount() {
+        return devLinkLookup.size();
+    }
+
+    /**
+     * Returns the number of edge links configured in the topology.
+     *
+     * @return number of edge links
+     */
+    public int edgeLinkCount() {
+        return edgeLinkLookup.size();
     }
 
     /**
@@ -405,22 +460,198 @@
     }
 
     /**
-     * Returns the set of UI links with the given identifiers.
+     * Returns the set of UI device links with the given identifiers.
      *
      * @param uiLinkIds link identifiers
-     * @return set of matching UI link instances
+     * @return set of matching UI device link instances
      */
-    Set<UiLink> linkSet(Set<UiLinkId> uiLinkIds) {
-        Set<UiLink> uiLinks = new HashSet<>();
+    Set<UiDeviceLink> linkSet(Set<UiLinkId> uiLinkIds) {
+        Set<UiDeviceLink> result = new HashSet<>();
         for (UiLinkId id : uiLinkIds) {
-            UiLink link = linkLookup.get(id);
+            UiDeviceLink link = devLinkLookup.get(id);
             if (link != null) {
-                uiLinks.add(link);
+                result.add(link);
             } else {
-                log.warn(E_UNMAPPED, "link", id);
+                log.warn(E_UNMAPPED, "device link", id);
             }
         }
-        return uiLinks;
+        return result;
+    }
+
+    /**
+     * Uses the device-device links and data about the regions to compute the
+     * set of synthetic links that are required per region.
+     */
+    public void computeSynthLinks() {
+        List<UiSynthLink> slinks = new ArrayList<>();
+        allDeviceLinks().forEach((link) -> {
+            UiSynthLink synthetic = inferSyntheticLink(link);
+            slinks.add(synthetic);
+            log.debug("Synthetic link: {}", synthetic);
+        });
+
+        synthLinks.clear();
+        synthLinks.addAll(slinks);
+    }
+
+    private UiSynthLink inferSyntheticLink(UiDeviceLink link) {
+        /*
+          Look at the containment hierarchy of each end of the link. Find the
+          common ancestor region R. A synthetic link will be added to R, based
+          on the "next" node back down the branch...
+
+                S1 --- S2       * in the same region ...
+                :      :
+                R      R          return S1 --- S2 (same link instance)
+
+
+                S1 --- S2       * in different regions (R1, R2) at same level
+                :      :
+                R1     R2         return R1 --- R2
+                :      :
+                R      R
+
+                S1 --- S2       * in different regions at different levels
+                :      :
+                R1     R2         return R1 --- R3
+                :      :
+                R      R3
+                       :
+                       R
+
+                S1 --- S2       * in different regions at different levels
+                :      :
+                R      R2         return S1 --- R2
+                       :
+                       R
+
+         */
+        DeviceId a = link.deviceA();
+        DeviceId b = link.deviceB();
+        List<RegionId> aBranch = ancestors(a);
+        List<RegionId> bBranch = ancestors(b);
+        if (aBranch == null || bBranch == null) {
+            return null;
+        }
+
+        return makeSynthLink(link, aBranch, bBranch);
+    }
+
+    // package private for unit testing
+    UiSynthLink makeSynthLink(UiDeviceLink orig,
+                              List<RegionId> aBranch,
+                              List<RegionId> bBranch) {
+
+        final int aSize = aBranch.size();
+        final int bSize = bBranch.size();
+        final int min = Math.min(aSize, bSize);
+
+        int index = 0;
+        RegionId commonRegion = aBranch.get(index);
+
+        while (true) {
+            int next = index + 1;
+            if (next == min) {
+                // no more pairs of regions left to test
+                break;
+            }
+            RegionId rA = aBranch.get(next);
+            RegionId rB = bBranch.get(next);
+            if (rA.equals(rB)) {
+                commonRegion = rA;
+                index++;
+            } else {
+                break;
+            }
+        }
+
+
+        int endPointIndex = index + 1;
+        UiLinkId linkId;
+        UiLink link;
+
+        if (endPointIndex < aSize) {
+            // the A endpoint is a subregion
+            RegionId aRegion = aBranch.get(endPointIndex);
+
+            if (endPointIndex < bSize) {
+                // the B endpoint is a subregion
+                RegionId bRegion = bBranch.get(endPointIndex);
+
+                linkId = uiLinkId(aRegion, bRegion);
+                link = new UiRegionLink(this, linkId);
+
+            } else {
+                // the B endpoint is the device
+                DeviceId dB = orig.deviceB();
+                PortNumber pB = orig.portB();
+
+                linkId = uiLinkId(aRegion, dB, pB);
+                link = new UiRegionDeviceLink(this, linkId);
+            }
+
+        } else {
+            // the A endpoint is the device
+            DeviceId dA = orig.deviceA();
+            PortNumber pA = orig.portA();
+
+            if (endPointIndex < bSize) {
+                // the B endpoint is a subregion
+                RegionId bRegion = bBranch.get(endPointIndex);
+
+                linkId = uiLinkId(bRegion, dA, pA);
+                link = new UiRegionDeviceLink(this, linkId);
+
+            } else {
+                // the B endpoint is the device
+                // (so, we can just use the original device-device link...)
+
+                link = orig;
+            }
+        }
+        return new UiSynthLink(commonRegion, link);
+    }
+
+    private List<RegionId> ancestors(DeviceId id) {
+        // return the ancestor chain from this device to root region
+        UiDevice dev = findDevice(id);
+        if (dev == null) {
+            log.warn("Unable to find cached device with ID %s", id);
+            return null;
+        }
+
+        UiRegion r = dev.uiRegion();
+        List<RegionId> result = new ArrayList<>();
+        while (r != null && !r.isRoot()) {
+            result.add(0, r.id());
+            r = r.parentRegion();
+        }
+        // finally add root region, since this is the grand-daddy of them all
+        result.add(0, UiRegion.NULL_ID);
+        return result;
+    }
+
+
+    /**
+     * Returns the synthetic links associated with the specified region.
+     *
+     * @param regionId the region ID
+     * @return synthetic links for this region
+     */
+    public List<UiSynthLink> findSynthLinks(RegionId regionId) {
+        return synthLinks.stream()
+                .filter(s -> Objects.equals(regionId, s.regionId()))
+                .collect(Collectors.toList());
+    }
+
+
+    /**
+     * Returns the number of synthetic links in the topology.
+     *
+     * @return the synthetic link count
+     */
+    public int synthLinkCount() {
+        return synthLinks.size();
     }
 
     /**
@@ -452,13 +683,22 @@
             sb.append(INDENT_2).append(h).append(EOL);
         }
 
-        sb.append(INDENT_1).append("Links").append(EOL);
-        for (UiLink link : linkLookup.values()) {
+        sb.append(INDENT_1).append("Device Links").append(EOL);
+        for (UiLink link : devLinkLookup.values()) {
+            sb.append(INDENT_2).append(link).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Edge Links").append(EOL);
+        for (UiLink link : edgeLinkLookup.values()) {
+            sb.append(INDENT_2).append(link).append(EOL);
+        }
+
+        sb.append(INDENT_1).append("Synth Links").append(EOL);
+        for (UiSynthLink link : synthLinks) {
             sb.append(INDENT_2).append(link).append(EOL);
         }
         sb.append("------").append(EOL);
 
         return sb.toString();
     }
-
 }
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiEdgeLinkTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiEdgeLinkTest.java
new file mode 100644
index 0000000..d5b213a
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiEdgeLinkTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.model.topo;
+
+import org.junit.Test;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultEdgeLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.EdgeLink;
+import org.onosproject.net.PortNumber;
+import org.onosproject.ui.model.AbstractUiModelTest;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link UiEdgeLink}.
+ */
+public class UiEdgeLinkTest extends AbstractUiModelTest {
+
+    private static final String PHANTOM_HOST_ID = "00:00:00:00:00:00/None";
+    private static final String D1_P8 = "dev-1/8";
+
+    private static final DeviceId DEV = DeviceId.deviceId("dev-1");
+    private static final PortNumber P8 = PortNumber.portNumber(8);
+    private static final ConnectPoint CP = new ConnectPoint(DEV, P8);
+
+    private static final EdgeLink EDGE_LINK =
+            DefaultEdgeLink.createEdgeLink(CP, true);
+
+    @Test
+    public void basic() {
+        title("basic");
+        UiLinkId id = UiLinkId.uiLinkId(EDGE_LINK);
+        UiEdgeLink link = new UiEdgeLink(null, id);
+        link.attachEdgeLink(EDGE_LINK);
+        print(link);
+        print(link.endPointA());
+        print(link.endPointB());
+
+        assertEquals("bad end point A", PHANTOM_HOST_ID, link.endPointA());
+        assertEquals("bad end point B", D1_P8, link.endPointB());
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
index e129e26..80666d6 100644
--- a/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiLinkIdTest.java
@@ -17,17 +17,23 @@
 package org.onosproject.ui.model.topo;
 
 import org.junit.Test;
+import org.onlab.packet.MacAddress;
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.DefaultLink;
 import org.onosproject.net.DeviceId;
+import org.onosproject.net.HostId;
 import org.onosproject.net.Link;
 import org.onosproject.net.PortNumber;
 import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.model.AbstractUiModelTest;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
 import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+import static org.onosproject.net.PortNumber.P0;
 import static org.onosproject.net.PortNumber.portNumber;
 
 /**
@@ -35,8 +41,15 @@
  */
 public class UiLinkIdTest extends AbstractUiModelTest {
 
+    private static final RegionId REG_1 = RegionId.regionId("Region-1");
+    private static final RegionId REG_2 = RegionId.regionId("Region-2");
+
+    private static final MacAddress MAC_A = MacAddress.valueOf(0x123456L);
+    private static final HostId HOST_A = hostId(MAC_A);
+
     private static final DeviceId DEV_X = deviceId("device-X");
     private static final DeviceId DEV_Y = deviceId("device-Y");
+
     private static final PortNumber P1 = portNumber(1);
     private static final PortNumber P2 = portNumber(2);
     private static final PortNumber P3 = portNumber(3);
@@ -45,6 +58,8 @@
     private static final ConnectPoint CP_Y2 = new ConnectPoint(DEV_Y, P2);
     private static final ConnectPoint CP_Y3 = new ConnectPoint(DEV_Y, P3);
 
+    private static final ConnectPoint CP_HA = new ConnectPoint(HOST_A, P0);
+
     private static final Link LINK_X1_TO_Y2 = DefaultLink.builder()
             .providerId(ProviderId.NONE)
             .src(CP_X1)
@@ -66,6 +81,12 @@
             .type(Link.Type.DIRECT)
             .build();
 
+    private static final Link LINK_HA_TO_X1 = DefaultLink.builder()
+            .providerId(ProviderId.NONE)
+            .src(CP_HA)
+            .dst(CP_X1)
+            .type(Link.Type.EDGE)
+            .build();
 
     @Test
     public void canonical() {
@@ -86,4 +107,61 @@
         print("link other: %s", other);
         assertNotEquals("equiv?", one, other);
     }
+
+    @Test
+    public void edgeLink() {
+        title("edgeLink");
+        UiLinkId id = UiLinkId.uiLinkId(LINK_HA_TO_X1);
+        print("link: %s", id);
+        assertEquals("wrong port A", P0, id.portA());
+        assertEquals("wrong element A", HOST_A, id.elementA());
+        assertEquals("wrong port B", P1, id.portB());
+        assertEquals("wrong element B", DEV_X, id.elementB());
+        assertNull("region A?", id.regionA());
+        assertNull("region B?", id.regionB());
+    }
+
+    @Test
+    public void deviceLink() {
+        title("deviceLink");
+        UiLinkId id = UiLinkId.uiLinkId(LINK_X1_TO_Y2);
+        print("link: %s", id);
+        assertEquals("wrong port A", P1, id.portA());
+        assertEquals("wrong element A", DEV_X, id.elementA());
+        assertEquals("wrong port B", P2, id.portB());
+        assertEquals("wrong element B", DEV_Y, id.elementB());
+        assertNull("region A?", id.regionA());
+        assertNull("region B?", id.regionB());
+    }
+
+    @Test
+    public void regionLink() {
+        title("regionLink");
+        UiLinkId idFirst = UiLinkId.uiLinkId(REG_1, REG_2);
+        UiLinkId idSecond = UiLinkId.uiLinkId(REG_2, REG_1);
+        print(" first: %s", idFirst);
+        print("second: %s", idSecond);
+        assertEquals("Not same ID", idFirst, idSecond);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void identicalRegionBad() {
+        UiLinkId.uiLinkId(REG_1, REG_1);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void nullRegionBad() {
+        UiLinkId.uiLinkId(REG_1, (RegionId) null);
+    }
+
+    @Test
+    public void regionDeviceLink() {
+        title("regionDeviceLink");
+        UiLinkId id = UiLinkId.uiLinkId(REG_1, DEV_X, P1);
+        print("id: %s", id);
+        assertEquals("region ID", REG_1, id.regionA());
+        assertEquals("device ID", DEV_X, id.elementB());
+        assertEquals("port", P1, id.portB());
+    }
+
 }
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionDeviceLinkTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionDeviceLinkTest.java
new file mode 100644
index 0000000..3fd70d1
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionDeviceLinkTest.java
@@ -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.
+ */
+
+package org.onosproject.ui.model.topo;
+
+import org.junit.Test;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.region.RegionId;
+import org.onosproject.ui.model.AbstractUiModelTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.P0;
+import static org.onosproject.net.region.RegionId.regionId;
+
+/**
+ * Unit tests for {@link UiRegionDeviceLink}.
+ */
+public class UiRegionDeviceLinkTest extends AbstractUiModelTest {
+
+    private static final RegionId R1 = regionId("r1");
+    private static final DeviceId DEV_X = deviceId("device-X");
+
+    @Test
+    public void basic() {
+        title("basic");
+        UiLinkId id = UiLinkId.uiLinkId(R1, DEV_X, P0);
+        UiRegionDeviceLink link = new UiRegionDeviceLink(null, id);
+        print(link);
+        assertEquals("region", R1, link.region());
+        assertEquals("device", DEV_X, link.device());
+        assertEquals("port", P0, link.port());
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionLinkTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionLinkTest.java
new file mode 100644
index 0000000..de00bd2
--- /dev/null
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiRegionLinkTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.ui.model.topo;
+
+import org.junit.Test;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.region.RegionId;
+import org.onosproject.ui.model.AbstractUiModelTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.P0;
+import static org.onosproject.net.region.RegionId.regionId;
+
+/**
+ * Unit tests for {@link UiRegionLink}.
+ */
+public class UiRegionLinkTest extends AbstractUiModelTest {
+
+    private static final RegionId R1 = regionId("r1");
+    private static final RegionId R2 = regionId("r2");
+
+    private static final DeviceId DEV_X = deviceId("device-X");
+    private static final DeviceId DEV_Y = deviceId("device-Y");
+
+    private static final ConnectPoint CP_X = new ConnectPoint(DEV_X, P0);
+    private static final ConnectPoint CP_Y = new ConnectPoint(DEV_Y, P0);
+
+    private static final Link LINK_X_TO_Y = DefaultLink.builder()
+            .providerId(ProviderId.NONE)
+            .src(CP_X)
+            .dst(CP_Y)
+            .type(Link.Type.DIRECT)
+            .build();
+
+
+    @Test(expected = NullPointerException.class)
+    public void nullPointerRegion() {
+        title("nullPointerRegion");
+        new UiRegionLink(null, null);
+    }
+
+    @Test
+    public void regionToRegion() {
+        title("regionToRegion");
+        UiLinkId id = UiLinkId.uiLinkId(R1, R2);
+        UiRegionLink link = new UiRegionLink(null, id);
+        print("link: %s", link);
+        assertEquals("bad first region", R1, link.regionA());
+        assertEquals("bad second region", R2, link.regionB());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void wrongLinkType() {
+        title("wrongLinkType");
+        UiLinkId id = UiLinkId.uiLinkId(LINK_X_TO_Y);
+        new UiRegionLink(null, id);
+    }
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/model/topo/UiTopologyTest.java b/core/api/src/test/java/org/onosproject/ui/model/topo/UiTopologyTest.java
index 17d2de7..5df0a9f 100644
--- a/core/api/src/test/java/org/onosproject/ui/model/topo/UiTopologyTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/model/topo/UiTopologyTest.java
@@ -16,20 +16,165 @@
 
 package org.onosproject.ui.model.topo;
 
+import org.junit.Before;
 import org.junit.Test;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultLink;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Link;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.region.RegionId;
 import org.onosproject.ui.AbstractUiTest;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.PortNumber.portNumber;
+
 /**
  * Unit tests for {@link UiTopology}.
  */
 public class UiTopologyTest extends AbstractUiTest {
 
+    private static final DeviceId DEV_X = deviceId("dev-X");
+    private static final DeviceId DEV_Y = deviceId("dev-Y");
+    private static final PortNumber P1 = portNumber(1);
+    private static final PortNumber P2 = portNumber(2);
+
+    private static final String DEV_X_ID = "dev-x/1";
+    private static final String DEV_Y_ID = "dev-y/2";
+
+    private static final ConnectPoint CP_X1 = new ConnectPoint(DEV_X, P1);
+    private static final ConnectPoint CP_Y2 = new ConnectPoint(DEV_Y, P2);
+
+    private static final Link LINK_X1_TO_Y2 = DefaultLink.builder()
+            .providerId(ProviderId.NONE)
+            .src(CP_X1)
+            .dst(CP_Y2)
+            .type(Link.Type.DIRECT)
+            .build();
+
+    private static final UiLinkId DX1_DY2 = UiLinkId.uiLinkId(LINK_X1_TO_Y2);
+
+    private static final RegionId ROOT = UiRegion.NULL_ID;
+    private static final RegionId R1 = RegionId.regionId("R1");
+    private static final RegionId R2 = RegionId.regionId("R2");
+    private static final RegionId R3 = RegionId.regionId("R3");
+
+    private static final String DEV_LINK_CLASS = "UiDeviceLink";
+    private static final String REG_LINK_CLASS = "UiRegionLink";
+    private static final String REG_DEV_LINK_CLASS = "UiRegionDeviceLink";
+
+
     private UiTopology topo;
+    private UiDeviceLink devLink;
+
+    private List<RegionId> xBranch;
+    private List<RegionId> yBranch;
+    private UiSynthLink synth;
+
+    @Before
+    public void setUp() {
+        topo = new UiTopology();
+        devLink = new UiDeviceLink(null, DX1_DY2);
+        devLink.attachBackingLink(LINK_X1_TO_Y2);
+    }
 
     @Test
     public void basic() {
         title("basic");
-        topo = new UiTopology();
         print(topo);
     }
+
+    private List<RegionId> branch(RegionId... ids) {
+        List<RegionId> result = new ArrayList<>(ids.length);
+        Collections.addAll(result, ids);
+        return result;
+    }
+
+    private void verifySynth(RegionId id, String cls, String epA, String epB) {
+        synth = topo.makeSynthLink(devLink, xBranch, yBranch);
+        UiLink ulink = synth.link();
+        print(synth);
+        print("EpA{%s}  EpB{%s}", ulink.endPointA(), ulink.endPointB());
+
+        assertEquals("wrong region", id, synth.regionId());
+        assertEquals("wrong link class", cls, ulink.type());
+        assertEquals("wrong EP A", epA, ulink.endPointA());
+        assertEquals("wrong EP B", epB, ulink.endPointB());
+    }
+
+    @Test
+    public void makeSynthDevToDevRoot() {
+        title("makeSynthDevToDevRoot");
+        xBranch = branch(ROOT);
+        yBranch = branch(ROOT);
+        verifySynth(ROOT, DEV_LINK_CLASS, DEV_X_ID, DEV_Y_ID);
+    }
+
+    @Test
+    public void makeSynthDevToDevR1() {
+        title("makeSynthDevToDevR1");
+        xBranch = branch(ROOT, R1);
+        yBranch = branch(ROOT, R1);
+        verifySynth(R1, DEV_LINK_CLASS, DEV_X_ID, DEV_Y_ID);
+    }
+
+    @Test
+    public void makeSynthDevToDevR2() {
+        title("makeSynthDevToDevR2");
+        xBranch = branch(ROOT, R1, R2);
+        yBranch = branch(ROOT, R1, R2);
+        verifySynth(R2, DEV_LINK_CLASS, DEV_X_ID, DEV_Y_ID);
+    }
+
+    @Test
+    public void makeSynthRegToRegRoot() {
+        title("makeSynthRegToRegRoot");
+        xBranch = branch(ROOT, R1);
+        yBranch = branch(ROOT, R2);
+        verifySynth(ROOT, REG_LINK_CLASS, R1.id(), R2.id());
+    }
+
+    @Test
+    public void makeSynthRegToRegR1() {
+        title("makeSynthRegToRegR1");
+        xBranch = branch(ROOT, R1, R2);
+        yBranch = branch(ROOT, R1, R3);
+        verifySynth(R1, REG_LINK_CLASS, R2.id(), R3.id());
+    }
+
+    @Test
+    public void makeSynthRegToDevRoot() {
+        title("makeSynthRegToDevRoot");
+
+        // Note: link is canonicalized to region--device order
+
+        xBranch = branch(ROOT);
+        yBranch = branch(ROOT, R1);
+        verifySynth(ROOT, REG_DEV_LINK_CLASS, R1.id(), DEV_X_ID);
+
+        xBranch = branch(ROOT, R1);
+        yBranch = branch(ROOT);
+        verifySynth(ROOT, REG_DEV_LINK_CLASS, R1.id(), DEV_Y_ID);
+    }
+
+    @Test
+    public void makeSynthRegToDevR3() {
+        title("makeSynthRegToDevR3");
+
+        // Note: link is canonicalized to region--device order
+
+        xBranch = branch(ROOT, R3);
+        yBranch = branch(ROOT, R3, R1);
+        verifySynth(R3, REG_DEV_LINK_CLASS, R1.id(), DEV_X_ID);
+
+        xBranch = branch(ROOT, R3, R1);
+        yBranch = branch(ROOT, R3);
+        verifySynth(R3, REG_DEV_LINK_CLASS, R1.id(), DEV_Y_ID);
+    }
 }
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 8a26a92..a5a344a 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
@@ -16,6 +16,7 @@
 
 package org.onosproject.ui.impl.topo;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -39,6 +40,7 @@
 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.UiSynthLink;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -180,9 +182,11 @@
      *
      * @param region     the region to transform to JSON
      * @param subRegions the subregions within this region
+     * @param links      the links within this region
      * @return a JSON representation of the data
      */
-    ObjectNode region(UiRegion region, Set<UiRegion> subRegions) {
+    ObjectNode region(UiRegion region, Set<UiRegion> subRegions,
+                      List<UiSynthLink> links) {
         ObjectNode payload = objectNode();
         if (region == null) {
             payload.put("note", "no-region");
@@ -193,14 +197,16 @@
             payload.set("subregions", jsonSubRegions(subRegions));
         }
 
+        if (links != null) {
+            payload.set("links", jsonLinks(links));
+        }
+
         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();
 
         payload.set("devices", jsonGrouped(splitDevices));
         payload.set("hosts", jsonGrouped(splitHosts));
-        payload.set("links", jsonLinks(links));
         payload.set("layerOrder", jsonStrings(layerTags));
 
         return payload;
@@ -208,24 +214,22 @@
 
     private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
         ArrayNode kids = arrayNode();
-        if (subregions != null) {
-            subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
-        }
+        subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
         return kids;
     }
 
+    private JsonNode jsonLinks(List<UiSynthLink> links) {
+        ArrayNode synthLinks = arrayNode();
+        links.forEach(l -> synthLinks.add(json(l)));
+        return synthLinks;
+    }
+
     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 -> {
@@ -280,11 +284,13 @@
         // TODO: complete host details
     }
 
-
-    private ObjectNode json(UiLink link) {
+    private ObjectNode json(UiSynthLink sLink) {
+        UiLink uLink = sLink.link();
         return objectNode()
-                .put("id", link.idAsString());
-        // TODO: complete link details
+                .put("id", uLink.idAsString())
+                .put("epA", uLink.endPointA())
+                .put("epB", uLink.endPointB())
+                .put("type", uLink.type());
     }
 
 
@@ -305,7 +311,7 @@
      */
     public ArrayNode closedNodes(Set<UiNode> nodes) {
         ArrayNode array = arrayNode();
-        for (UiNode node: nodes) {
+        for (UiNode node : nodes) {
             if (node instanceof UiRegion) {
                 array.add(jsonClosedRegion((UiRegion) node));
             } else if (node instanceof UiDevice) {
@@ -361,20 +367,6 @@
         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) {
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 7795271..86d54cc 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
@@ -26,6 +26,7 @@
 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.UiSynthLink;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -125,7 +126,8 @@
             //   (as well as layer-order hints)
             UiRegion region = topoSession.getRegion(currentLayout);
             Set<UiRegion> kids = topoSession.getSubRegions(currentLayout);
-            sendMessage(CURRENT_REGION, t2json.region(region, kids));
+            List<UiSynthLink> links = topoSession.getLinks(currentLayout);
+            sendMessage(CURRENT_REGION, t2json.region(region, kids, links));
 
             // these are the regions/devices that are siblings to this region
             Set<UiNode> peers = topoSession.getPeerNodes(currentLayout);
@@ -133,6 +135,8 @@
             peersPayload.set("peers", t2json.closedNodes(peers));
             sendMessage(PEER_REGIONS, peersPayload);
 
+            // TODO: send breadcrumb message
+
             // finally, tell the UI that we are done : TODO review / delete??
             sendMessage(TOPO_START_DONE, null);
 
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 7127ec1..44ea910 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
@@ -25,6 +25,7 @@
 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.UiSynthLink;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -214,6 +215,16 @@
     }
 
     /**
+     * Returns the (synthetic) links of the region in the specified layout.
+     *
+     * @param layout the layout being viewed
+     * @return all links that are contained by this layout's region
+     */
+    public List<UiSynthLink> getLinks(UiTopoLayout layout) {
+        return sharedModel.getSynthLinks(layout.regionId());
+    }
+
+    /**
      * Refreshes the model's internal state.
      */
     public void refreshModel() {
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListLinks.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListLinks.java
index 640b37d..245aab3 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListLinks.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/cli/ListLinks.java
@@ -29,6 +29,6 @@
     @Override
     protected void execute() {
         UiSharedTopologyModel model = get(UiSharedTopologyModel.class);
-        sorted(model.getLinks()).forEach(l -> print("%s", l));
+        sorted(model.getDeviceLinks()).forEach(l -> print("%s", l));
     }
 }
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 781650d..02c21dc 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
@@ -33,11 +33,13 @@
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
+import org.onosproject.ui.model.topo.UiDeviceLink;
+import org.onosproject.ui.model.topo.UiEdgeLink;
 import org.onosproject.ui.model.topo.UiElement;
 import org.onosproject.ui.model.topo.UiHost;
-import org.onosproject.ui.model.topo.UiLink;
 import org.onosproject.ui.model.topo.UiLinkId;
 import org.onosproject.ui.model.topo.UiRegion;
+import org.onosproject.ui.model.topo.UiSynthLink;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 import org.onosproject.ui.model.topo.UiTopoLayoutId;
 import org.onosproject.ui.model.topo.UiTopology;
@@ -101,7 +103,7 @@
         loadClusterMembers();
         loadRegions();
         loadDevices();
-        loadLinks();
+        loadDeviceLinks();
         loadHosts();
     }
 
@@ -334,66 +336,72 @@
     }
 
 
-    // === LINKS
+    // === LINKS ===
 
-    private UiLink addNewLink(UiLinkId id) {
-        UiLink uiLink = new UiLink(uiTopology, id);
-        uiTopology.add(uiLink);
-        return uiLink;
+    private UiDeviceLink addNewDeviceLink(UiLinkId id) {
+        UiDeviceLink uiDeviceLink = new UiDeviceLink(uiTopology, id);
+        uiTopology.add(uiDeviceLink);
+        return uiDeviceLink;
     }
 
-    private void updateLink(UiLink uiLink, Link link) {
-        uiLink.attachBackingLink(link);
+    private UiEdgeLink addNewEdgeLink(UiLinkId id) {
+        UiEdgeLink uiEdgeLink = new UiEdgeLink(uiTopology, id);
+        uiTopology.add(uiEdgeLink);
+        return uiEdgeLink;
     }
 
-    private void loadLinks() {
+    private void updateDeviceLink(UiDeviceLink uiDeviceLink, Link link) {
+        uiDeviceLink.attachBackingLink(link);
+    }
+
+    private void loadDeviceLinks() {
         for (Link link : services.link().getLinks()) {
             UiLinkId id = uiLinkId(link);
 
-            UiLink uiLink = uiTopology.findLink(id);
-            if (uiLink == null) {
-                uiLink = addNewLink(id);
+            UiDeviceLink uiDeviceLink = uiTopology.findDeviceLink(id);
+            if (uiDeviceLink == null) {
+                uiDeviceLink = addNewDeviceLink(id);
             }
-            updateLink(uiLink, link);
+            updateDeviceLink(uiDeviceLink, link);
         }
     }
 
     // invoked from UiSharedTopologyModel link listener
-    void addOrUpdateLink(Link link) {
+    void addOrUpdateDeviceLink(Link link) {
         UiLinkId id = uiLinkId(link);
-        UiLink uiLink = uiTopology.findLink(id);
-        if (uiLink == null) {
-            uiLink = addNewLink(id);
+        UiDeviceLink uiDeviceLink = uiTopology.findDeviceLink(id);
+        if (uiDeviceLink == null) {
+            uiDeviceLink = addNewDeviceLink(id);
         }
-        updateLink(uiLink, link);
+        updateDeviceLink(uiDeviceLink, link);
 
-        postEvent(LINK_ADDED_OR_UPDATED, uiLink);
+        postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink);
     }
 
     // package private for unit test access
-    UiLink accessLink(UiLinkId id) {
-        return uiTopology.findLink(id);
+    UiDeviceLink accessDeviceLink(UiLinkId id) {
+        return uiTopology.findDeviceLink(id);
     }
 
     // invoked from UiSharedTopologyModel link listener
-    void removeLink(Link link) {
+    void removeDeviceLink(Link link) {
         UiLinkId id = uiLinkId(link);
-        UiLink uiLink = uiTopology.findLink(id);
-        if (uiLink != null) {
-            boolean remaining = uiLink.detachBackingLink(link);
+        UiDeviceLink uiDeviceLink = uiTopology.findDeviceLink(id);
+        if (uiDeviceLink != null) {
+            boolean remaining = uiDeviceLink.detachBackingLink(link);
             if (remaining) {
-                postEvent(LINK_ADDED_OR_UPDATED, uiLink);
+                postEvent(LINK_ADDED_OR_UPDATED, uiDeviceLink);
             } else {
-                uiTopology.remove(uiLink);
-                postEvent(LINK_REMOVED, uiLink);
+                uiTopology.remove(uiDeviceLink);
+                postEvent(LINK_REMOVED, uiDeviceLink);
             }
         } else {
-            log.warn(E_NO_ELEMENT, "link", id);
+            log.warn(E_NO_ELEMENT, "Device link", id);
         }
     }
 
-    Set<UiLink> getAllLinks() {
-        return uiTopology.allLinks();
+    Set<UiDeviceLink> getAllDeviceLinks() {
+        return uiTopology.allDeviceLinks();
     }
 
     // === HOSTS
@@ -411,20 +419,19 @@
         host.setEdgeLinkId(elinkId);
 
         // add synthesized edge link to the topology
-        UiLink edgeLink = addNewLink(elinkId);
+        UiEdgeLink edgeLink = addNewEdgeLink(elinkId);
         edgeLink.attachEdgeLink(elink);
 
         return host;
     }
 
-    private void insertNewUiLink(UiLinkId id, EdgeLink e) {
-        UiLink newEdgeLink = addNewLink(id);
+    private void insertNewUiEdgeLink(UiLinkId id, EdgeLink e) {
+        UiEdgeLink newEdgeLink = addNewEdgeLink(id);
         newEdgeLink.attachEdgeLink(e);
-
     }
 
     private void updateHost(UiHost uiHost, Host h) {
-        UiLink existing = uiTopology.findLink(uiHost.edgeLinkId());
+        UiEdgeLink existing = uiTopology.findEdgeLink(uiHost.edgeLinkId());
 
         EdgeLink currentElink = synthesizeLink(h);
         UiLinkId currentElinkId = uiLinkId(currentElink);
@@ -432,7 +439,7 @@
         if (existing != null) {
             if (!currentElinkId.equals(existing.id())) {
                 // edge link has changed
-                insertNewUiLink(currentElinkId, currentElink);
+                insertNewUiEdgeLink(currentElinkId, currentElink);
                 uiHost.setEdgeLinkId(currentElinkId);
 
                 uiTopology.remove(existing);
@@ -440,7 +447,7 @@
 
         } else {
             // no previously existing edge link
-            insertNewUiLink(currentElinkId, currentElink);
+            insertNewUiEdgeLink(currentElinkId, currentElink);
             uiHost.setEdgeLinkId(currentElinkId);
 
         }
@@ -489,7 +496,7 @@
         HostId id = host.id();
         UiHost uiHost = uiTopology.findHost(id);
         if (uiHost != null) {
-            UiLink edgeLink = uiTopology.findLink(uiHost.edgeLinkId());
+            UiEdgeLink edgeLink = uiTopology.findEdgeLink(uiHost.edgeLinkId());
             uiTopology.remove(edgeLink);
             uiTopology.remove(uiHost);
             postEvent(HOST_REMOVED, uiHost);
@@ -503,11 +510,17 @@
     }
 
 
+    // === SYNTHETIC LINKS
+
+    List<UiSynthLink> getSynthLinks(RegionId regionId) {
+        return uiTopology.findSynthLinks(regionId);
+    }
+
     /**
      * Refreshes the internal state.
      */
     public void refresh() {
-        // fix up internal linkages if they aren't correct
+        // fix up internal linkages to ensure they are correct
 
         // make sure regions reflect layout containment hierarchy
         fixupContainmentHierarchy(uiTopology.nullRegion());
@@ -542,8 +555,13 @@
         Set<DeviceId> leftOver = new HashSet<>(allDevices.size());
         allDevices.forEach(d -> leftOver.add(d.id()));
         uiTopology.nullRegion().reconcileDevices(leftOver);
+
+        // now that we have correct region hierarchy, and devices are in their
+        //  respective regions, we can compute synthetic links for each region.
+        uiTopology.computeSynthLinks();
     }
 
+
     // === CACHE STATISTICS
 
     /**
@@ -583,12 +601,21 @@
     }
 
     /**
-     * Returns the number of links in the topology.
+     * Returns the number of device links in the topology.
      *
-     * @return number of links
+     * @return number of device links
      */
-    public int linkCount() {
-        return uiTopology.linkCount();
+    public int deviceLinkCount() {
+        return uiTopology.deviceLinkCount();
+    }
+
+    /**
+     * Returns the number of edge links in the topology.
+     *
+     * @return number of edge links
+     */
+    public int edgeLinkCount() {
+        return uiTopology.edgeLinkCount();
     }
 
     /**
@@ -600,4 +627,12 @@
         return uiTopology.hostCount();
     }
 
+    /**
+     * Returns the number of synthetic links in the topology.
+     *
+     * @return the number of synthetic links
+     */
+    public int synthLinkCount() {
+        return uiTopology.synthLinkCount();
+    }
 }
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 a12e34d..2a66b8e 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
@@ -65,9 +65,10 @@
 import org.onosproject.ui.model.ServiceBundle;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
+import org.onosproject.ui.model.topo.UiDeviceLink;
 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.UiSynthLink;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -269,12 +270,22 @@
     }
 
     /**
-     * Returns the set of links stored in the model cache.
+     * Returns the set of device links stored in the model cache.
      *
-     * @return set of links
+     * @return set of device links
      */
-    public Set<UiLink> getLinks() {
-        return cache.getAllLinks();
+    public Set<UiDeviceLink> getDeviceLinks() {
+        return cache.getAllDeviceLinks();
+    }
+
+    /**
+     * Returns the synthetic links associated with the specified region.
+     *
+     * @param regionId region ID
+     * @return synthetic links for that region
+     */
+    public List<UiSynthLink> getSynthLinks(RegionId regionId) {
+        return cache.getSynthLinks(regionId);
     }
 
     // =====================================================================
@@ -434,11 +445,11 @@
 
                 case LINK_ADDED:
                 case LINK_UPDATED:
-                    cache.addOrUpdateLink(link);
+                    cache.addOrUpdateDeviceLink(link);
                     break;
 
                 case LINK_REMOVED:
-                    cache.removeLink(link);
+                    cache.removeDeviceLink(link);
                     break;
 
                 default:
diff --git a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
index 4d409dc..d7ec3b6 100644
--- a/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
+++ b/web/gui/src/test/java/org/onosproject/ui/impl/topo/model/ModelCacheTest.java
@@ -29,9 +29,9 @@
 import org.onosproject.ui.impl.topo.model.UiModelEvent.Type;
 import org.onosproject.ui.model.topo.UiClusterMember;
 import org.onosproject.ui.model.topo.UiDevice;
+import org.onosproject.ui.model.topo.UiDeviceLink;
 import org.onosproject.ui.model.topo.UiElement;
 import org.onosproject.ui.model.topo.UiHost;
-import org.onosproject.ui.model.topo.UiLink;
 import org.onosproject.ui.model.topo.UiLinkId;
 import org.onosproject.ui.model.topo.UiRegion;
 
@@ -265,54 +265,54 @@
         // we've established that the ID is the same for both
         UiLinkId linkId = idA2B;
 
-        cache.addOrUpdateLink(link1);
+        cache.addOrUpdateDeviceLink(link1);
         dispatcher.assertLast(Type.LINK_ADDED_OR_UPDATED, linkId.toString());
         dispatcher.assertEventCount(1);
-        assertEquals("unex # links", 1, cache.linkCount());
+        assertEquals("unex # links", 1, cache.deviceLinkCount());
 
-        UiLink link = cache.accessLink(linkId);
+        UiDeviceLink link = cache.accessDeviceLink(linkId);
         assertEquals("dev A not d2", DEVID_2, link.deviceA());
         assertEquals("dev B not d7", DEVID_7, link.deviceB());
         assertEquals("wrong backing link A-B", link1, link.linkAtoB());
         assertEquals("backing link B-A?", null, link.linkBtoA());
 
-        cache.addOrUpdateLink(link2);
+        cache.addOrUpdateDeviceLink(link2);
         dispatcher.assertLast(Type.LINK_ADDED_OR_UPDATED, linkId.toString());
         dispatcher.assertEventCount(2);
         // NOTE: yes! expect 1 UiLink
-        assertEquals("unex # links", 1, cache.linkCount());
+        assertEquals("unex # links", 1, cache.deviceLinkCount());
 
-        link = cache.accessLink(linkId);
+        link = cache.accessDeviceLink(linkId);
         assertEquals("dev A not d2", DEVID_2, link.deviceA());
         assertEquals("dev B not d7", DEVID_7, link.deviceB());
         assertEquals("wrong backing link A-B", link1, link.linkAtoB());
         assertEquals("wrong backing link B-A", link2, link.linkBtoA());
 
         // now remove links one at a time
-        cache.removeLink(link1);
+        cache.removeDeviceLink(link1);
         // NOTE: yes! ADD_OR_UPDATE, since the link was updated
         dispatcher.assertLast(Type.LINK_ADDED_OR_UPDATED, linkId.toString());
         dispatcher.assertEventCount(3);
         // NOTE: yes! expect 1 UiLink (still)
-        assertEquals("unex # links", 1, cache.linkCount());
+        assertEquals("unex # links", 1, cache.deviceLinkCount());
 
-        link = cache.accessLink(linkId);
+        link = cache.accessDeviceLink(linkId);
         assertEquals("dev A not d2", DEVID_2, link.deviceA());
         assertEquals("dev B not d7", DEVID_7, link.deviceB());
         assertEquals("backing link A-B?", null, link.linkAtoB());
         assertEquals("wrong backing link B-A", link2, link.linkBtoA());
 
         // remove final link
-        cache.removeLink(link2);
+        cache.removeDeviceLink(link2);
         dispatcher.assertLast(Type.LINK_REMOVED, linkId.toString());
         dispatcher.assertEventCount(4);
         // NOTE: finally link should be removed from cache
-        assertEquals("unex # links", 0, cache.linkCount());
+        assertEquals("unex # links", 0, cache.deviceLinkCount());
     }
 
     private void assertHostLinkCounts(int nHosts, int nLinks) {
         assertEquals("unex # hosts", nHosts, cache.hostCount());
-        assertEquals("unex # links", nLinks, cache.linkCount());
+        assertEquals("unex # links", nLinks, cache.edgeLinkCount());
     }
 
     private void assertLocation(HostId hid, DeviceId expDev, int expPort) {
@@ -403,6 +403,8 @@
         assertEquals("unex # regions", 3, cache.regionCount());
         assertEquals("unex # devices", 9, cache.deviceCount());
         assertEquals("unex # hosts", 18, cache.hostCount());
-        assertEquals("unex # hosts", 26, cache.linkCount());
+        assertEquals("unex # device-links", 8, cache.deviceLinkCount());
+        assertEquals("unex # edge-links", 18, cache.edgeLinkCount());
+        assertEquals("unex # synth-links", 0, cache.synthLinkCount());
     }
 }