GUI -- [ONOS-309] - Oblique view of packet and optical layers (Experimental).

Change-Id: I5e3ac53192eb6c0d7bfab599fe2254f58f192a50
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_10_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_10_onos.json
new file mode 100644
index 0000000..10f868f
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_10_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "2-2b",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "sw2",
+    "srcPort": "20",
+    "dst": "sw2b",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_11_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_11_onos.json
new file mode 100644
index 0000000..deaf0a1
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_11_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "3-3b",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "sw3",
+    "srcPort": "20",
+    "dst": "sw3b",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_12_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_12_onos.json
new file mode 100644
index 0000000..ff47e5c
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_12_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "4-4b",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "sw4",
+    "srcPort": "20",
+    "dst": "sw4b",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_1_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_1_onos.json
new file mode 100644
index 0000000..780f0aa
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_1_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw1b",
+    "type": "roadm",
+    "online": true,
+    "labels": [
+      "",
+      "sw-1b",
+      "00001b"
+    ],
+    "metaUi": {
+      "x": 200,
+      "y": 200
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_2_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_2_onos.json
new file mode 100644
index 0000000..e0dba2b
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_2_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw2b",
+    "type": "roadm",
+    "online": true,
+    "labels": [
+      "",
+      "sw-2b",
+      "00002b"
+    ],
+    "metaUi": {
+      "x": 800,
+      "y": 200
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_3_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_3_onos.json
new file mode 100644
index 0000000..38d066a
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_3_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw3b",
+    "type": "roadm",
+    "online": true,
+    "labels": [
+      "",
+      "sw-3b",
+      "00003b"
+    ],
+    "metaUi": {
+      "x": 200,
+      "y": 600
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_4_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_4_onos.json
new file mode 100644
index 0000000..bece53d
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_4_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw4b",
+    "type": "roadm",
+    "online": true,
+    "labels": [
+      "",
+      "sw-4b",
+      "00004b"
+    ],
+    "metaUi": {
+      "x": 800,
+      "y": 600
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_5_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_5_onos.json
new file mode 100644
index 0000000..a05f3f9
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_5_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw1",
+    "type": "switch",
+    "online": true,
+    "labels": [
+      "",
+      "sw-1",
+      "00001"
+    ],
+    "metaUi": {
+      "x": 200,
+      "y": 200
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_6_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_6_onos.json
new file mode 100644
index 0000000..27e3b14
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_6_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw2",
+    "type": "switch",
+    "online": true,
+    "labels": [
+      "",
+      "sw-2",
+      "00002"
+    ],
+    "metaUi": {
+      "x": 800,
+      "y": 200
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_7_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_7_onos.json
new file mode 100644
index 0000000..992b964
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_7_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw3",
+    "type": "switch",
+    "online": true,
+    "labels": [
+      "",
+      "sw-3",
+      "00003"
+    ],
+    "metaUi": {
+      "x": 200,
+      "y": 600
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_8_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_8_onos.json
new file mode 100644
index 0000000..2c33d50
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_8_onos.json
@@ -0,0 +1,17 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "sw4",
+    "type": "switch",
+    "online": true,
+    "labels": [
+      "",
+      "sw-4",
+      "00004"
+    ],
+    "metaUi": {
+      "x": 800,
+      "y": 600
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/ev_9_onos.json b/web/gui/src/main/webapp/json/ev/oblique/ev_9_onos.json
new file mode 100644
index 0000000..57e9706
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/ev_9_onos.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "1-1b",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "sw1",
+    "srcPort": "20",
+    "dst": "sw1b",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/main/webapp/json/ev/oblique/scenario.json b/web/gui/src/main/webapp/json/ev/oblique/scenario.json
new file mode 100644
index 0000000..7b349ea
--- /dev/null
+++ b/web/gui/src/main/webapp/json/ev/oblique/scenario.json
@@ -0,0 +1,11 @@
+{
+  "title": "Oblique Test Scenario",
+  "params": {
+    "lastAuto": 8
+  },
+  "description": [
+    "Test Scenario for Oblique view",
+    "",
+    "Press '=' to load initial events."
+  ]
+}
diff --git a/web/gui/src/main/webapp/topo.js b/web/gui/src/main/webapp/topo.js
index d2e67bf..704ade1 100644
--- a/web/gui/src/main/webapp/topo.js
+++ b/web/gui/src/main/webapp/topo.js
@@ -149,7 +149,7 @@
         A: [showAllTrafficAction, 'Show all traffic'],
         F: [showDeviceLinkFlowsAction, 'Show device link flows'],
         X: [toggleNodeLock, 'Lock / unlock node positions'],
-        Z: [toggleOblique, 'Toggle oblique view'],
+        Z: [toggleOblique, 'Toggle oblique view (Experimental)'],
         esc: handleEscape
     };
 
@@ -326,6 +326,12 @@
         bgImg.style('visibility', visVal(vis === 'hidden'));
     }
 
+    function opacifyBg(b) {
+        bgImg.transition()
+            .duration(1000)
+            .attr('opacity', b ? 1 : 0);
+    }
+
     function toggleNodeLock() {
         nodeLock = !nodeLock;
         flash('Node positions ' + (nodeLock ? 'locked' : 'unlocked'))
@@ -333,7 +339,12 @@
 
     function toggleOblique() {
         oblique = !oblique;
-        // TODO: oblique transformation
+        if (oblique) {
+            network.force.stop();
+            toObliqueView();
+        } else {
+            toNormalView();
+        }
     }
 
     function toggleHosts() {
@@ -368,7 +379,7 @@
             sendUpdateMeta(hovered);
             hovered.fixed = false;
             hovered.el.classed('fixed', false);
-            network.force.resume();
+            fResume();
         }
     }
 
@@ -388,6 +399,179 @@
     }
 
     // ==============================
+    // Oblique view ...
+
+    var obview = {
+            tt:  -.7,     // x skew y factor
+            xsk: -35,     // x skew angle
+            ysc: 0.5,     // y scale
+            pad: 50,
+            time: 1500,
+            fill: {
+                pkt: 'rgba(130,130,170,0.3)',
+                opt: 'rgba(170,130,170,0.3)'
+            },
+            id: function (tag) {
+                return 'obview-' + tag + 'Plane';
+            },
+            yt: function (h, dir) {
+                return h * obview.ysc * dir * 1.1;
+            },
+            obXform: function (h, dir) {
+                var yt = obview.yt(h, dir);
+                return scale(1, obview.ysc) + translate(0, yt) + skewX(obview.xsk);
+            },
+            noXform: function () {
+                return skewX(0) + translate(0,0) + scale(1,1);
+            },
+            xffn: null,
+            plane: {}
+    };
+
+
+    function toObliqueView() {
+        var box = nodeG.node().getBBox(),
+            ox, oy;
+
+        padBox(box, obview.pad);
+
+        ox = box.x + box.width / 2;
+        oy = box.y + box.height / 2;
+
+        // remember node lock state, then lock the nodes down
+        obview.nodeLock = nodeLock;
+        nodeLock = true;
+        opacifyBg(false);
+
+        insertPlanes(ox, oy);
+
+        obview.xffn = function (xy, dir) {
+            var yt = obview.yt(box.height, dir),
+                ax = xy.x - ox,
+                ay = xy.y - oy,
+                x = ax + ay * obview.tt,
+                y = ay * obview.ysc + obview.ysc * yt;
+            return {x: ox + x, y: oy + y};
+        };
+
+        showPlane('pkt', box, -1);
+        showPlane('opt', box, 1);
+        obTransitionNodes();
+    }
+
+    function toNormalView() {
+        obview.xffn = null;
+
+        hidePlane('pkt');
+        hidePlane('opt');
+        obTransitionNodes();
+
+        removePlanes();
+
+        // restore node lock state
+        nodeLock = obview.nodeLock;
+        opacifyBg(true);
+    }
+
+    function obTransitionNodes() {
+        var xffn = obview.xffn;
+
+        // return the direction for the node
+        // -1 for pkt layer, 1 for optical layer
+        function dir(d) {
+            return inLayer(d, 'pkt') ? -1 : 1;
+        }
+
+        if (xffn) {
+            network.nodes.forEach(function (d) {
+                var oldxy = {x: d.x, y: d.y},
+                    coords = xffn(oldxy, dir(d));
+                d.oldxy = oldxy;
+                d.px = d.x = coords.x;
+                d.py = d.y = coords.y;
+            });
+        } else {
+            network.nodes.forEach(function (d) {
+                var old = d.oldxy || {x: d.x, y: d.y};
+                d.px = d.x = old.x;
+                d.py = d.y = old.y;
+                delete d.oldxy;
+            });
+        }
+
+        node.transition()
+            .duration(obview.time)
+            .attr(tickStuff.nodeAttr);
+        link.transition()
+            .duration(obview.time)
+            .attr(tickStuff.linkAttr);
+        linkLabel.transition()
+            .duration(obview.time)
+            .attr(tickStuff.linkLabelAttr);
+    }
+
+    function showPlane(tag, box, dir) {
+        var g = obview.plane[tag];
+
+        // set box origin at center..
+        box.x = -box.width/2;
+        box.y = -box.height/2;
+
+        g.select('rect')
+            .attr(box)
+            .attr('opacity', 0)
+            .transition()
+            .duration(obview.time)
+            .attr('opacity', 1)
+            .attr('transform', obview.obXform(box.height, dir));
+    }
+
+    function hidePlane(tag) {
+        var g = obview.plane[tag];
+
+        g.select('rect')
+            .transition()
+            .duration(obview.time)
+            .attr('opacity', 0)
+            .attr('transform', obview.noXform());
+    }
+
+    function insertPlanes(ox, oy) {
+        function ins(tag) {
+            var id = obview.id(tag),
+                g = panZoomContainer.insert('g', '#topo-G')
+                    .attr('id', id)
+                    .attr('transform', translate(ox,oy));
+            g.append('rect')
+                .attr('fill', obview.fill[tag])
+                .attr('opacity', 0);
+            obview.plane[tag] = g;
+        }
+        ins('opt');
+        ins('pkt');
+    }
+
+    function removePlanes() {
+        function rem(tag) {
+            var id = obview.id(tag);
+            panZoomContainer.select('#'+id)
+                .transition()
+                .duration(obview.time + 50)
+                .remove();
+            delete obview.plane[tag];
+        }
+        rem('opt');
+        rem('pkt');
+    }
+
+    function padBox(box, p) {
+        box.x -= p;
+        box.y -= p;
+        box.width += p*2;
+        box.height += p*2;
+    }
+
+    // ==============================
     // Radio Button Callbacks
 
     var layerLookup = {
@@ -651,7 +835,7 @@
         network.nodes.push(d);
         network.lookup[id] = d;
         updateNodes();
-        network.force.start();
+        fStart();
     }
 
     function addLink(data) {
@@ -678,7 +862,7 @@
             network.links.push(d);
             network.lookup[d.key] = d;
             updateLinks();
-            network.force.start();
+            fStart();
         }
     }
 
@@ -707,7 +891,7 @@
             network.lookup[d.egress] = lnk;
             updateLinks();
         }
-        network.force.start();
+        fStart();
     }
 
     // TODO: fold updateX(...) methods into one base method; remove duplication
@@ -1332,7 +1516,12 @@
     function translate(x, y) {
         return 'translate(' + x + ',' + y + ')';
     }
-
+    function scale(x,y) {
+        return 'scale(' + x + ',' + y + ')';
+    }
+    function skewX(x) {
+        return 'skewX(' + x + ')';
+    }
     function rotate(deg) {
         return 'rotate(' + deg + ')';
     }
@@ -1965,8 +2154,7 @@
                 .style('fill', '#888')
                 .style('opacity', 0.5);
         });
-
-        network.force.resume();
+        fResume();
     }
 
     var dCol = {
@@ -2091,7 +2279,7 @@
             // remove from lookup cache
             delete network.lookup[removed[0].key];
             updateLinks();
-            network.force.resume();
+            fResume();
         }
     }
 
@@ -2113,7 +2301,7 @@
         // NOTE: upd is false if we were called from removeDeviceElement()
         if (upd) {
             updateNodes();
-            network.force.resume();
+            fResume();
         }
     }
 
@@ -2131,7 +2319,7 @@
         network.nodes.splice(idx, 1);
         // remove from SVG
         updateNodes();
-        network.force.resume();
+        fResume();
     }
 
     function findAttachedHosts(devId) {
@@ -2154,32 +2342,49 @@
         return links;
     }
 
-    function tick() {
-        node.attr({
-            transform: function (d) { return translate(d.x, d.y); }
-        });
+    function fResume() {
+        if (!oblique) {
+            network.force.resume();
+        }
+    }
 
-        link.attr({
+    function fStart() {
+        if (!oblique) {
+            network.force.start();
+        }
+    }
+
+    var tickStuff = {
+        nodeAttr: {
+            transform: function (d) { return translate(d.x, d.y); }
+        },
+        linkAttr: {
             x1: function (d) { return d.source.x; },
             y1: function (d) { return d.source.y; },
             x2: function (d) { return d.target.x; },
             y2: function (d) { return d.target.y; }
-        });
+        },
+        linkLabelAttr: {
+            transform: function (d) {
+                var lnk = findLinkById(d.key);
 
-        linkLabel.each(function (d) {
-            var el = d3.select(this);
-            var lnk = findLinkById(d.key);
-
-            if (lnk) {
-                var parms = {
-                    x1: lnk.source.x,
-                    y1: lnk.source.y,
-                    x2: lnk.target.x,
-                    y2: lnk.target.y
-                };
-                el.attr('transform', transformLabel(parms));
+                if (lnk) {
+                    var parms = {
+                        x1: lnk.source.x,
+                        y1: lnk.source.y,
+                        x2: lnk.target.x,
+                        y2: lnk.target.y
+                    };
+                    return transformLabel(parms);
+                }
             }
-        });
+        }
+    };
+
+    function tick() {
+        node.attr(tickStuff.nodeAttr);
+        link.attr(tickStuff.linkAttr);
+        linkLabel.attr(tickStuff.linkLabelAttr);
     }
 
     // ==============================