GUI -- TopoView - Re-instated the oblique view function. (Keystroke 'Z').

Change-Id: I1bc4c11590660142a6bc9f5f71c06a664dbfa80b
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 89e2706..998f442 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -204,6 +204,11 @@
         return ms.loadMapInto(mapG, '*continental_us');
     }
 
+    function opacifyMap(b) {
+        mapG.transition()
+            .duration(1000)
+            .attr('opacity', b ? 1 : 0);
+    }
 
     // --- Controller Definition -----------------------------------------
 
@@ -226,6 +231,8 @@
                     // provides function calls back into this space
                     showNoDevs: showNoDevs,
                     projection: function () { return projection; },
+                    zoomLayer: function () { return zoomLayer; },
+                    opacifyMap: opacifyMap,
                     sendEvent: _tes_.sendEvent
                 };
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoFilter.js b/web/gui/src/main/webapp/app/view/topo/topoFilter.js
index c2e7a62..53a6302 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoFilter.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoFilter.js
@@ -117,7 +117,6 @@
         return btnG ? btnG.selected : '';
     }
 
-    // code to manipulate the nodes and links as per the filter settings
     function inLayer(d, layer) {
         var type = d.class === 'link' ? d.type() : d.type,
             look = layerLookup[d.class],
@@ -186,7 +185,8 @@
                     destroyFilter: destroyFilter,
 
                     clickAction: clickAction,
-                    selected: selected
+                    selected: selected,
+                    inLayer: inLayer
                 };
             }]);
 }());
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index 80a7b63..e2d1809 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -76,7 +76,6 @@
         hostLabelIndex = 0,     // for host label cycling
         showHosts = false,      // whether hosts are displayed
         showOffline = true,     // whether offline devices are displayed
-        oblique = false,        // whether we are in the oblique view
         nodeLock = false,       // whether nodes can be dragged or not (locked)
         dim;                    // the dimensions of the force layout [w,h]
 
@@ -965,13 +964,13 @@
     // force layout tick function
 
     function fResume() {
-        if (!oblique) {
+        if (!tos.isOblique()) {
             force.resume();
         }
     }
 
     function fStart() {
-        if (!oblique) {
+        if (!tos.isOblique()) {
             force.start();
         }
     }
@@ -1084,10 +1083,23 @@
         }
     }
 
-    function mkObliqueApi(uplink) {
+    function mkObliqueApi(uplink, fltr) {
         return {
+            force: function() { return force; },
+            zoomLayer: uplink.zoomLayer,
+            nodeGBBox: function() { return nodeG.node().getBBox(); },
             node: function () { return node; },
-            link: function () { return link; }
+            link: function () { return link; },
+            linkLabel: function () { return linkLabel; },
+            nodes: function () { return network.nodes; },
+            tickStuff: tickStuff,
+            nodeLock: function (b) {
+                var old = nodeLock;
+                nodeLock = b;
+                return old;
+            },
+            opacifyMap: uplink.opacifyMap,
+            inLayer: fltr.inLayer
         };
     }
 
@@ -1140,7 +1152,7 @@
                 tms.initModel(mkModelApi(uplink), dim);
                 tss.initSelect(mkSelectApi(uplink));
                 tts.initTraffic(mkTrafficApi(uplink));
-                tos.initOblique(mkObliqueApi(uplink));
+                tos.initOblique(mkObliqueApi(uplink, fltr));
                 fltr.initFilter(mkFilterApi(uplink), d3.select('#mast-right'));
 
                 settings = angular.extend({}, defaultSettings, opts);
diff --git a/web/gui/src/main/webapp/app/view/topo/topoOblique.js b/web/gui/src/main/webapp/app/view/topo/topoOblique.js
index 4b7bc50..8a30862 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOblique.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOblique.js
@@ -24,35 +24,206 @@
     'use strict';
 
     // injected refs
-    var $log, fs;
+    var $log, fs, sus, ts;
 
     // api to topoForce
     var api;
     /*
+     force()                        // get ref to force layout object
+     zoomLayer()                    // get ref to zoom layer
+     nodeGBBox()                    // get bounding box of node group layer
      node()                         // get ref to D3 selection of nodes
      link()                         // get ref to D3 selection of links
+     nodes()                        // get ref to network nodes array
+     tickStuff                      // ref to tick functions
+     nodeLock(b)                    // test-and-set nodeLock state
+     opacifyMap(b)                  // show or hide map layer
+     inLayer(d, layer)              // return true if d in layer {'pkt'|'opt'}
      */
 
+    // configuration
+    var xsky = -.7,     // x skew y factor
+        xsk = -35,      // x skew angle
+        ysc = .5,       // y scale
+        pad = 50,
+        time = 1500,
+        fill = {
+            pkt: 'rgba(130,130,170,0.3)',   // blue-ish
+            opt: 'rgba(170,130,170,0.3)'    // magenta-ish
+        };
+
     // internal state
-    var foo;
-
-    // ==========================
+    var oblique = false,
+        xffn = null,
+        plane = {},
+        oldNodeLock;
 
 
-    function toggleOblique() {
-        $log.log("TOGGLING OBLIQUE VIEW");
+    function planeId(tag) {
+        return 'topo-obview-' + tag + 'Plane';
     }
 
+    function ytfn(h, dir) {
+        return h * ysc * dir * 1.1;
+    }
+
+    function obXform(h, dir) {
+        var yt = ytfn(h, dir);
+        return sus.scale(1, ysc) + sus.translate(0, yt) + sus.skewX(xsk);
+    }
+
+    function noXform() {
+        return sus.skewX(0) + sus.translate(0,0) + sus.scale(1,1);
+    }
+
+    function padBox(box, p) {
+        box.x -= p;
+        box.y -= p;
+        box.width += p*2;
+        box.height += p*2;
+    }
+
+    function toObliqueView() {
+        var box = api.nodeGBBox(),
+            ox, oy;
+
+        padBox(box, pad);
+
+        ox = box.x + box.width / 2;
+        oy = box.y + box.height / 2;
+
+        // remember node lock state, then lock the nodes down
+        oldNodeLock = api.nodeLock(true);
+        api.opacifyMap(false);
+
+        insertPlanes(ox, oy);
+
+        xffn = function (xy, dir) {
+            var yt = ytfn(box.height, dir),
+                ax = xy.x - ox,
+                ay = xy.y - oy,
+                x = ax + ay * xsky,
+                y = (ay + yt) * ysc;
+            return {x: ox + x, y: oy + y};
+        };
+
+        showPlane('pkt', box, -1);
+        showPlane('opt', box, 1);
+        obTransitionNodes();
+    }
+
+    function toNormalView() {
+        xffn = null;
+
+        hidePlane('pkt');
+        hidePlane('opt');
+        obTransitionNodes();
+
+        removePlanes();
+
+        // restore node lock state
+        api.nodeLock(oldNodeLock);
+        api.opacifyMap(true);
+    }
+
+    function obTransitionNodes() {
+        // return the direction for the node
+        // -1 for pkt layer, 1 for optical layer
+        function dir(d) {
+            return api.inLayer(d, 'pkt') ? -1 : 1;
+        }
+
+        if (xffn) {
+            api.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 {
+            api.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;
+            });
+        }
+
+        api.node().transition()
+            .duration(time)
+            .attr(api.tickStuff.nodeAttr);
+        api.link().transition()
+            .duration(time)
+            .attr(api.tickStuff.linkAttr);
+        api.linkLabel().transition()
+            .duration(time)
+            .attr(api.tickStuff.linkLabelAttr);
+    }
+
+    function showPlane(tag, box, dir) {
+        // set box origin at center..
+        box.x = -box.width/2;
+        box.y = -box.height/2;
+
+        plane[tag].select('rect')
+            .attr(box)
+            .attr('opacity', 0)
+            .transition()
+            .duration(time)
+            .attr('opacity', 1)
+            .attr('transform', obXform(box.height, dir));
+    }
+
+    function hidePlane(tag) {
+        plane[tag].select('rect')
+            .transition()
+            .duration(time)
+            .attr('opacity', 0)
+            .attr('transform', noXform());
+    }
+
+    function insertPlanes(ox, oy) {
+        function ins(tag) {
+            var id = planeId(tag),
+                g = api.zoomLayer().insert('g', '#topo-G')
+                    .attr('id', id)
+                    .attr('transform', sus.translate(ox,oy));
+            g.append('rect')
+                .attr('fill', fill[tag])
+                .attr('opacity', 0);
+            plane[tag] = g;
+        }
+        ins('opt');
+        ins('pkt');
+    }
+
+    function removePlanes() {
+        function rem(tag) {
+            var id = planeId(tag);
+            api.zoomLayer().select('#'+id)
+                .transition()
+                .duration(time + 50)
+                .remove();
+            delete plane[tag];
+        }
+        rem('opt');
+        rem('pkt');
+    }
+
+
 // === -----------------------------------------------------
 // === MODULE DEFINITION ===
 
 angular.module('ovTopo')
     .factory('TopoObliqueService',
-    ['$log', 'FnService',
+    ['$log', 'FnService', 'SvgUtilService', 'ThemeService',
 
-    function (_$log_, _fs_) {
+    function (_$log_, _fs_, _sus_, _ts_) {
         $log = _$log_;
         fs = _fs_;
+        sus = _sus_;
+        ts = _ts_;
 
         function initOblique(_api_) {
             api = _api_;
@@ -60,10 +231,21 @@
 
         function destroyOblique() { }
 
+        function toggleOblique() {
+            oblique = !oblique;
+            if (oblique) {
+                api.force().stop();
+                toObliqueView();
+            } else {
+                toNormalView();
+            }
+        }
+
         return {
             initOblique: initOblique,
             destroyOblique: destroyOblique,
 
+            isOblique: function () { return oblique; },
             toggleOblique: toggleOblique
         };
     }]);
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
index 79a4398..a9c7ddd 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoFilter-spec.js
@@ -54,7 +54,7 @@
     it('should define api functions', function () {
         expect(fs.areFunctions(fltr, [
             'initFilter', 'destroyFilter',
-            'clickAction', 'selected'
+            'clickAction', 'selected', 'inLayer',
         ])).toBeTruthy();
     });
 
diff --git a/web/gui/src/main/webapp/tests/app/view/topo/topoOblique-spec.js b/web/gui/src/main/webapp/tests/app/view/topo/topoOblique-spec.js
index 5b8e120..bf07569 100644
--- a/web/gui/src/main/webapp/tests/app/view/topo/topoOblique-spec.js
+++ b/web/gui/src/main/webapp/tests/app/view/topo/topoOblique-spec.js
@@ -34,7 +34,7 @@
 
     it('should define api functions', function () {
         expect(fs.areFunctions(tos, [
-            'initOblique', 'destroyOblique', 'toggleOblique'
+            'initOblique', 'destroyOblique', 'isOblique', 'toggleOblique'
         ])).toBeTruthy();
     });