Merge "GUI -- Test events: unpinned the first node; increased a few link widths.  - added alerts pane to framework.  - added library registration mechanism to framework.  - created d3Utils library  - reimplemented drag behavior of nodes."
diff --git a/web/gui/src/main/webapp/d3Utils.js b/web/gui/src/main/webapp/d3Utils.js
new file mode 100644
index 0000000..51651fa
--- /dev/null
+++ b/web/gui/src/main/webapp/d3Utils.js
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Utility functions for D3 visualizations.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    function createDragBehavior(force, selectCb, atDragEnd) {
+        var draggedThreshold = d3.scale.linear()
+                .domain([0, 0.1])
+                .range([5, 20])
+                .clamp(true),
+            drag;
+
+        // TODO: better validation of parameters
+        if (!$.isFunction(selectCb)) {
+            alert('d3util.createDragBehavior(): selectCb is not a function')
+        }
+        if (!$.isFunction(atDragEnd)) {
+            alert('d3util.createDragBehavior(): atDragEnd is not a function')
+        }
+
+        function dragged(d) {
+            var threshold = draggedThreshold(force.alpha()),
+                dx = d.oldX - d.px,
+                dy = d.oldY - d.py;
+            if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+                d.dragged = true;
+            }
+            return d.dragged;
+        }
+
+        drag = d3.behavior.drag()
+            .origin(function(d) { return d; })
+            .on('dragstart', function(d) {
+                d.oldX = d.x;
+                d.oldY = d.y;
+                d.dragged = false;
+                d.fixed |= 2;
+            })
+            .on('drag', function(d) {
+                d.px = d3.event.x;
+                d.py = d3.event.y;
+                if (dragged(d)) {
+                    if (!force.alpha()) {
+                        force.alpha(.025);
+                    }
+                }
+            })
+            .on('dragend', function(d) {
+                if (!dragged(d)) {
+                    // consider this the same as a 'click' (selection of node)
+                    selectCb(d, this); // TODO: set 'this' context instead of param
+                }
+                d.fixed &= ~6;
+
+                // hook at the end of a drag gesture
+                atDragEnd(d, this); // TODO: set 'this' context instead of param
+            });
+
+        return drag;
+    }
+
+    function appendGlow(svg) {
+        // TODO: parameterize color
+
+        var glow = svg.append('filter')
+            .attr('x', '-50%')
+            .attr('y', '-50%')
+            .attr('width', '200%')
+            .attr('height', '200%')
+            .attr('id', 'blue-glow');
+
+        glow.append('feColorMatrix')
+            .attr('type', 'matrix')
+            .attr('values', '0 0 0 0  0 ' +
+            '0 0 0 0  0 ' +
+            '0 0 0 0  .7 ' +
+            '0 0 0 1  0 ');
+
+        glow.append('feGaussianBlur')
+            .attr('stdDeviation', 3)
+            .attr('result', 'coloredBlur');
+
+        glow.append('feMerge').selectAll('feMergeNode')
+            .data(['coloredBlur', 'SourceGraphic'])
+            .enter().append('feMergeNode')
+            .attr('in', String);
+    }
+
+    // === register the functions as a library
+    onos.ui.addLib('d3util', {
+        createDragBehavior: createDragBehavior,
+        appendGlow: appendGlow
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
index 1ddc318..235c1d7 100644
--- a/web/gui/src/main/webapp/index2.html
+++ b/web/gui/src/main/webapp/index2.html
@@ -64,6 +64,9 @@
         <div id="overlays">
             <!-- NOTE: overlays injected here, as needed -->
         </div>
+        <div id="alerts">
+            <!-- NOTE: alert content injected here, as needed -->
+        </div>
     </div>
 
     <!-- Initialize the UI...-->
@@ -76,6 +79,9 @@
         });
     </script>
 
+    <!-- Library module files included here -->
+    <script src="d3Utils.js"></script>
+
     <!-- Framework module files included here -->
     <script src="mast2.js"></script>
 
diff --git a/web/gui/src/main/webapp/json/eventTest_11.json b/web/gui/src/main/webapp/json/eventTest_11.json
index e907444..3b361d5 100644
--- a/web/gui/src/main/webapp/json/eventTest_11.json
+++ b/web/gui/src/main/webapp/json/eventTest_11.json
@@ -10,8 +10,8 @@
       "?"
     ],
     "metaUi": {
-      "x": 832,
-      "y": 223
+      "Zx": 832,
+      "Zy": 223
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/eventTest_15.json b/web/gui/src/main/webapp/json/eventTest_15.json
index 3622122..30ba9f3 100644
--- a/web/gui/src/main/webapp/json/eventTest_15.json
+++ b/web/gui/src/main/webapp/json/eventTest_15.json
@@ -10,8 +10,8 @@
       "?"
     ],
     "metaUi": {
-      "x": 840,
-      "y": 290
+      "Zx": 840,
+      "Zy": 290
     }
   }
 }
diff --git a/web/gui/src/main/webapp/json/eventTest_17.json b/web/gui/src/main/webapp/json/eventTest_17.json
index e217456..82272a4 100644
--- a/web/gui/src/main/webapp/json/eventTest_17.json
+++ b/web/gui/src/main/webapp/json/eventTest_17.json
@@ -6,7 +6,7 @@
     "dst": "of:0000ffffffffff05",
     "dstPort": "10",
     "type": "optical",
-    "linkWidth": 2,
+    "linkWidth": 6,
     "props" : {
       "BW": "80 G"
     }
diff --git a/web/gui/src/main/webapp/json/eventTest_23.json b/web/gui/src/main/webapp/json/eventTest_23.json
index 54383bc..fff0f2b 100644
--- a/web/gui/src/main/webapp/json/eventTest_23.json
+++ b/web/gui/src/main/webapp/json/eventTest_23.json
@@ -6,7 +6,7 @@
     "dst": "of:0000ffffffffff05",
     "dstPort": "30",
     "type": "optical",
-    "linkWidth": 2,
+    "linkWidth": 6,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/eventTest_24.json b/web/gui/src/main/webapp/json/eventTest_24.json
index 2287f6c..756b6c1 100644
--- a/web/gui/src/main/webapp/json/eventTest_24.json
+++ b/web/gui/src/main/webapp/json/eventTest_24.json
@@ -6,7 +6,7 @@
     "dst": "of:0000ffffffffff08",
     "dstPort": "20",
     "type": "optical",
-    "linkWidth": 2,
+    "linkWidth": 6,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/eventTest_30.json b/web/gui/src/main/webapp/json/eventTest_30.json
index ae2d4c1..a617f45 100644
--- a/web/gui/src/main/webapp/json/eventTest_30.json
+++ b/web/gui/src/main/webapp/json/eventTest_30.json
@@ -6,7 +6,7 @@
     "dst": "of:0000ffffffffff08",
     "dstPort": "30",
     "type": "optical",
-    "linkWidth": 2,
+    "linkWidth": 6,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/eventTest_34.json b/web/gui/src/main/webapp/json/eventTest_34.json
index 96015b5..fa5e3bc 100644
--- a/web/gui/src/main/webapp/json/eventTest_34.json
+++ b/web/gui/src/main/webapp/json/eventTest_34.json
@@ -6,7 +6,7 @@
     "dst": "of:0000ffffffffff08",
     "dstPort": "10",
     "type": "optical",
-    "linkWidth": 2,
+    "linkWidth": 6,
     "props" : {
       "BW": "70 G"
     }
diff --git a/web/gui/src/main/webapp/json/eventTest_35.json b/web/gui/src/main/webapp/json/eventTest_35.json
new file mode 100644
index 0000000..c579e59
--- /dev/null
+++ b/web/gui/src/main/webapp/json/eventTest_35.json
@@ -0,0 +1,14 @@
+{
+  "event": "addLink",
+  "payload": {
+    "src": "of:0000ffffffffff04",
+    "srcPort": "27",
+    "dst": "of:0000ffffffffff08",
+    "dstPort": "10",
+    "type": "optical",
+    "linkWidth": 2,
+    "props" : {
+      "BW": "30 G"
+    }
+  }
+}
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
index 983f288..748cc97 100644
--- a/web/gui/src/main/webapp/onos2.css
+++ b/web/gui/src/main/webapp/onos2.css
@@ -32,6 +32,34 @@
     display: block;
 }
 
+div#alerts {
+    display: none;
+    position: absolute;
+    z-index: 2000;
+    opacity: 0.65;
+    background-color: #006;
+    color: white;
+    top: 80px;
+    left: 40px;
+    padding: 3px 6px;
+    box-shadow: 4px 6px 12px #777;
+}
+
+div#alerts pre {
+    margin: 0.2em 6px;
+}
+
+div#alerts span.close {
+    color: #6af;
+    float: right;
+    right: 2px;
+    cursor: pointer;
+}
+
+div#alerts span.close:hover {
+    color: #fff;
+}
+
 /*
  * ==============================================================
  * END OF NEW ONOS.JS file
@@ -54,12 +82,6 @@
  * Network Graph elements ======================================
  */
 
-svg .link {
-    opacity: .7;
-}
-
-svg .link.host {
-}
 
 svg g.portLayer rect.port {
     fill: #ccc;
@@ -70,33 +92,13 @@
     pointer-events: none;
 }
 
-svg .node.device rect {
-    stroke-width: 1.5px;
-}
 
-svg .node.device.fixed rect {
-    stroke-width: 1.5;
-    stroke: #ccc;
-}
-
-svg .node.device.roadm rect {
-    fill: #03c;
-}
-
-svg .node.device.switch rect {
-    fill: #06f;
-}
 
 svg .node.host circle {
     fill: #c96;
     stroke: #000;
 }
 
-svg .node text {
-    fill: white;
-    font: 10pt sans-serif;
-    pointer-events: none;
-}
 
 /* for debugging */
 svg .node circle.debug {
@@ -110,10 +112,6 @@
 }
 
 
-svg .node.selected rect,
-svg .node.selected circle {
-    filter: url(#blue-glow);
-}
 
 svg .link.inactive,
 svg .port.inactive,
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
index 375fe6b..31d89fa 100644
--- a/web/gui/src/main/webapp/onos2.js
+++ b/web/gui/src/main/webapp/onos2.js
@@ -32,7 +32,8 @@
     $.onos = function (options) {
         var uiApi,
             viewApi,
-            navApi;
+            navApi,
+            libApi;
 
         var defaultOptions = {
             trace: false,
@@ -331,6 +332,58 @@
             }
         }
 
+        var alerts = {
+            open: false,
+            count: 0
+        };
+
+        function createAlerts() {
+            var al = d3.select('#alerts')
+                .style('display', 'block');
+            al.append('span')
+                .attr('class', 'close')
+                .text('X')
+                .on('click', closeAlerts);
+            al.append('pre');
+            alerts.open = true;
+            alerts.count = 0;
+        }
+
+        function closeAlerts() {
+            d3.select('#alerts')
+                .style('display', 'none');
+            d3.select('#alerts span').remove();
+            d3.select('#alerts pre').remove();
+            alerts.open = false;
+        }
+
+        function addAlert(msg) {
+            var lines,
+                oldContent;
+
+            if (alerts.count) {
+                oldContent = d3.select('#alerts pre').html();
+            }
+
+            lines = msg.split('\n');
+            lines[0] += '  '; // spacing so we don't crowd 'X'
+            lines = lines.join('\n');
+
+            if (oldContent) {
+                lines += '\n----\n' + oldContent;
+            }
+
+            d3.select('#alerts pre').html(lines);
+            alerts.count++;
+        }
+
+        function doAlert(msg) {
+            if (!alerts.open) {
+                createAlerts();
+            }
+            addAlert(msg);
+        }
+
         function keyIn() {
             var event = d3.event,
                 keyCode = event.keyCode,
@@ -408,7 +461,8 @@
                     uid: this.uid,
                     setRadio: this.setRadio,
                     setKeys: this.setKeys,
-                    dataLoadError: this.dataLoadError
+                    dataLoadError: this.dataLoadError,
+                    alert: this.alert
                 }
             },
 
@@ -501,14 +555,20 @@
                 return makeUid(this, id);
             },
 
-            // TODO : implement custom dialogs (don't use alerts)
+            // TODO : implement custom dialogs
+
+            // Consider enhancing alert mechanism to handle multiples
+            // as individually closable.
+            alert: function (msg) {
+                doAlert(msg);
+            },
 
             dataLoadError: function (err, url) {
                 var msg = 'Data Load Error\n\n' +
                     err.status + ' -- ' + err.statusText + '\n\n' +
                     'relative-url: "' + url + '"\n\n' +
                     'complete-url: "' + err.responseURL + '"';
-                alert(msg);
+                this.alert(msg);
             }
 
             // TODO: consider schedule, clearTimer, etc.
@@ -521,6 +581,12 @@
         // UI API
 
         uiApi = {
+            addLib: function (libName, api) {
+                // TODO: validation of args
+                libApi[libName] = api;
+            },
+
+            // TODO: it remains to be seen whether we keep this style of docs
             /** @api ui addView( vid, nid, cb )
              * Adds a view to the UI.
              * <p>
@@ -590,6 +656,12 @@
         };
 
         // ..........................................................
+        // Library API
+        libApi = {
+
+        };
+
+        // ..........................................................
         // Exported API
 
         // function to be called from index.html to build the ONOS UI
@@ -623,7 +695,8 @@
         // export the api and build-UI function
         return {
             ui: uiApi,
-            view: viewApi,
+            lib: libApi,
+            //view: viewApi,
             nav: navApi,
             buildUi: buildOnosUi
         };
diff --git a/web/gui/src/main/webapp/topo2.css b/web/gui/src/main/webapp/topo2.css
index 8293ce4..c4d9a2d 100644
--- a/web/gui/src/main/webapp/topo2.css
+++ b/web/gui/src/main/webapp/topo2.css
@@ -24,12 +24,18 @@
     opacity: 0.5;
 }
 
+/* NODES */
+
 svg .node.device {
     stroke: none;
     stroke-width: 1.5px;
     cursor: pointer;
 }
 
+svg .node.device rect {
+    stroke-width: 1.5px;
+}
+
 svg .node.device.fixed rect {
     stroke-width: 1.5;
     stroke: #ccc;
@@ -50,6 +56,17 @@
     pointer-events: none;
 }
 
+svg .node.selected rect,
+svg .node.selected circle {
+    filter: url(#blue-glow);
+}
+
+/* LINKS */
+
+svg .link {
+    opacity: .7;
+}
+
 /* for debugging */
 svg .node circle.debug {
     fill: white;
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
index bd5a577..d345fda 100644
--- a/web/gui/src/main/webapp/topo2.js
+++ b/web/gui/src/main/webapp/topo2.js
@@ -23,6 +23,9 @@
 (function (onos) {
     'use strict';
 
+    // shorter names for library APIs
+    var d3u = onos.lib.d3util;
+
     // configuration data
     var config = {
         useLiveData: false,
@@ -61,6 +64,10 @@
                 height: 14
             }
         },
+        topo: {
+            linkInColor: '#66f',
+            linkInWidth: 14
+        },
         icons: {
             w: 28,
             h: 28,
@@ -106,7 +113,9 @@
     // key bindings
     var keyDispatch = {
         space: injectTestEvent,     // TODO: remove (testing only)
- //       M: testMe,                  // TODO: remove (testing only)
+        S: injectStartupEvents,     // TODO: remove (testing only)
+        A: testAlert,               // TODO: remove (testing only)
+        M: testMe,                  // TODO: remove (testing only)
 
         B: toggleBg,
         G: toggleLayout,
@@ -141,7 +150,8 @@
     // For Debugging / Development
 
     var eventPrefix = 'json/eventTest_',
-        eventNumber = 0;
+        eventNumber = 0,
+        alertNumber = 0;
 
     function note(label, msg) {
         console.log('NOTE: ' + label + ': ' + msg);
@@ -155,22 +165,12 @@
     // ==============================
     // Key Callbacks
 
+    function testAlert(view) {
+        alertNumber++;
+        view.alert("Test me! -- " + alertNumber);
+    }
+
     function testMe(view) {
-        svg.append('line')
-            .attr({
-                x1: 100,
-                y1: 100,
-                x2: 500,
-                y2: 400,
-                stroke: '#2f3',
-                'stroke-width': 8
-            })
-            .transition()
-            .duration(1200)
-            .attr({
-                stroke: '#666',
-                'stroke-width': 6
-            });
     }
 
     function injectTestEvent(view) {
@@ -187,6 +187,13 @@
         });
     }
 
+    function injectStartupEvents(view) {
+        var lastStartupEvent = 32;
+        while (eventNumber < lastStartupEvent) {
+            injectTestEvent(view);
+        }
+    }
+
     function toggleBg() {
         var vis = bgImg.style('visibility');
         bgImg.style('visibility', (vis === 'hidden') ? 'visible' : 'hidden');
@@ -370,6 +377,11 @@
         return lnk;
     }
 
+    function linkWidth(w) {
+        // w is number of links between nodes. Scale appropriately.
+        return w * 1.2;
+    }
+
     function updateLinks() {
         link = linkG.selectAll('.link')
             .data(network.links, function (d) { return d.id; });
@@ -387,12 +399,12 @@
                 y1: function (d) { return d.y1; },
                 x2: function (d) { return d.x2; },
                 y2: function (d) { return d.y2; },
-                stroke: '#66f',
-                'stroke-width': 10
+                stroke: config.topo.linkInColor,
+                'stroke-width': config.topo.linkInWidth
             })
             .transition().duration(1000)
             .attr({
-                'stroke-width': function (d) { return d.width; },
+                'stroke-width': function (d) { return linkWidth(d.width); },
                 stroke: '#666'      // TODO: remove explicit stroke, rather...
             });
 
@@ -461,6 +473,10 @@
         return box;
     }
 
+    function mkSvgClass(d) {
+        return d.fixed ? d.svgClass + ' fixed' : d.svgClass;
+    }
+
     function updateNodes() {
         node = nodeG.selectAll('.node')
             .data(network.nodes, function (d) { return d.id; });
@@ -473,11 +489,11 @@
             .append('g')
             .attr({
                 id: function (d) { return safeId(d.id); },
-                class: function (d) { return d.svgClass; },
+                class: mkSvgClass,
                 transform: function (d) { return translate(d.x, d.y); },
                 opacity: 0
             })
-            //.call(network.drag)
+            .call(network.drag)
             //.on('mouseover', function (d) {})
             //.on('mouseover', function (d) {})
             .transition()
@@ -578,6 +594,9 @@
         svg = view.$div.append('svg');
         setSize(svg, view);
 
+        // add blue glow filter to svg layer
+        d3u.appendGlow(svg);
+
         // load the background image
         bgImg = svg.append('svg:image')
             .attr({
@@ -612,6 +631,20 @@
             return fcfg.charge[d.class] || -200;
         }
 
+        function selectCb(d, self) {
+            // TODO: selectObject(d, self);
+        }
+
+        function atDragEnd(d, self) {
+            // once we've finished moving, pin the node in position,
+            // if it is a device (not a host)
+            if (d.class === 'device') {
+                d.fixed = true;
+                d3.select(self).classed('fixed', true)
+                // TODO: send new [x,y] back to server, via websocket.
+            }
+        }
+
         // set up the force layout
         network.force = d3.layout.force()
             .size(forceDim)
@@ -621,8 +654,9 @@
             .linkDistance(ldist)
             .linkStrength(lstrg)
             .on('tick', tick);
-    }
 
+        network.drag = d3u.createDragBehavior(network.force, selectCb, atDragEnd);
+    }
 
     function load(view, ctx) {
         // cache the view token, so network topo functions can access it