Merge branch 'p4u/luci-app-bmx7/refactory'
[feed/routing.git] / luci-app-bmx7 / files / www / luci-static / resources / bmx7 / js / netjsongraph.js
diff --git a/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js b/luci-app-bmx7/files/www/luci-static/resources/bmx7/js/netjsongraph.js
new file mode 100644 (file)
index 0000000..66d0a5f
--- /dev/null
@@ -0,0 +1,568 @@
+// version 0.1
+(function () {
+    /**
+     * vanilla JS implementation of jQuery.extend()
+     */
+    d3._extend = function(defaults, options) {
+        var extended = {},
+            prop;
+        for(prop in defaults) {
+            if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
+                extended[prop] = defaults[prop];
+            }
+        }
+        for(prop in options) {
+            if(Object.prototype.hasOwnProperty.call(options, prop)) {
+                extended[prop] = options[prop];
+            }
+        }
+        return extended;
+    };
+
+    /**
+      * @function
+      *   @name d3._pxToNumber
+      * Convert strings like "10px" to 10
+      *
+      * @param  {string}       val         The value to convert
+      * @return {int}              The converted integer
+      */
+    d3._pxToNumber = function(val) {
+        return parseFloat(val.replace('px'));
+    };
+
+    /**
+      * @function
+      *   @name d3._windowHeight
+      *
+      * Get window height
+      *
+      * @return  {int}            The window innerHeight
+      */
+    d3._windowHeight = function() {
+        return window.innerHeight || document.documentElement.clientHeight || 600;
+    };
+
+    /**
+      * @function
+      *   @name d3._getPosition
+      *
+      * Get the position of `element` relative to `container`
+      *
+      * @param  {object}      element
+      * @param  {object}      container
+      */
+     d3._getPosition = function(element, container) {
+         var n = element.node(),
+             nPos = n.getBoundingClientRect();
+             cPos = container.node().getBoundingClientRect();
+         return {
+            top: nPos.top - cPos.top,
+            left: nPos.left - cPos.left,
+            width: nPos.width,
+            bottom: nPos.bottom - cPos.top,
+            height: nPos.height,
+            right: nPos.right - cPos.left
+        };
+     };
+
+    /**
+     * netjsongraph.js main function
+     *
+     * @constructor
+     * @param  {string}      url             The NetJSON file url
+     * @param  {object}      opts            The object with parameters to override {@link d3.netJsonGraph.opts}
+     */
+    d3.netJsonGraph = function(url, opts) {
+        /**
+         * Default options
+         *
+         * @param  {string}     el                  "body"      The container element                                  el: "body" [description]
+         * @param  {bool}       metadata            true        Display NetJSON metadata at startup?
+         * @param  {bool}       defaultStyle        true        Use css style?
+         * @param  {bool}       animationAtStart    false       Animate nodes or not on load
+         * @param  {array}      scaleExtent         [0.25, 5]   The zoom scale's allowed range. @see {@link https://github.com/mbostock/d3/wiki/Zoom-Behavior#scaleExtent}
+         * @param  {int}        charge              -130        The charge strength to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#charge}
+         * @param  {int}        linkDistance        50          The target distance between linked nodes to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkDistance}
+         * @param  {float}      linkStrength        0.2         The strength (rigidity) of links to the specified value in the range. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#linkStrength}
+         * @param  {float}      friction            0.9         The friction coefficient to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#friction}
+         * @param  {string}     chargeDistance      Infinity    The maximum distance over which charge forces are applied. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#chargeDistance}
+         * @param  {float}      theta               0.8         The Barnes–Hut approximation criterion to the specified value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#theta}
+         * @param  {float}      gravity             0.1         The gravitational strength to the specified numerical value. @see {@link https://github.com/mbostock/d3/wiki/Force-Layout#gravity}
+         * @param  {int}        circleRadius        8           The radius of circles (nodes) in pixel
+         * @param  {string}     labelDx             "0"         SVG dx (distance on x axis) attribute of node labels in graph
+         * @param  {string}     labelDy             "-1.3em"    SVG dy (distance on y axis) attribute of node labels in graph
+         * @param  {function}   onInit                          Callback function executed on initialization
+         * @param  {function}   onLoad                          Callback function executed after data has been loaded
+         * @param  {function}   onEnd                           Callback function executed when initial animation is complete
+         * @param  {function}   linkDistanceFunc                By default high density areas have longer links
+         * @param  {function}   redraw                          Called when panning and zooming
+         * @param  {function}   prepareData                     Used to convert NetJSON NetworkGraph to the javascript data
+         * @param  {function}   onClickNode                     Called when a node is clicked
+         * @param  {function}   onClickLink                     Called when a link is clicked
+         */
+        opts = d3._extend({
+            el: "body",
+            metadata: true,
+            defaultStyle: true,
+            animationAtStart: true,
+            scaleExtent: [0.25, 5],
+            charge: -130,
+            linkDistance: 50,
+            linkStrength: 0.2,
+            friction: 0.9,  // d3 default
+            chargeDistance: Infinity,  // d3 default
+            theta: 0.8,  // d3 default
+            gravity: 0.1,
+            circleRadius: 8,
+            labelDx: "0",
+            labelDy: "-1.3em",
+            nodeClassProperty: null,
+            linkClassProperty: null,
+            /**
+             * @function
+             * @name onInit
+             *
+             * Callback function executed on initialization
+             * @param  {string|object}  url     The netJson remote url or object
+             * @param  {object}         opts    The object of passed arguments
+             * @return {function}
+             */
+            onInit: function(url, opts) {},
+            /**
+             * @function
+             * @name onLoad
+             *
+             * Callback function executed after data has been loaded
+             * @param  {string|object}  url     The netJson remote url or object
+             * @param  {object}         opts    The object of passed arguments
+             * @return {function}
+             */
+            onLoad: function(url, opts) {},
+            /**
+             * @function
+             * @name onEnd
+             *
+             * Callback function executed when initial animation is complete
+             * @param  {string|object}  url     The netJson remote url or object
+             * @param  {object}         opts    The object of passed arguments
+             * @return {function}
+             */
+            onEnd: function(url, opts) {},
+            /**
+             * @function
+             * @name linkDistanceFunc
+             *
+             * By default, high density areas have longer links
+             */
+            linkDistanceFunc: function(d){
+                var val = opts.linkDistance;
+                if(d.source.linkCount >= 4 && d.target.linkCount >= 4) {
+                    return val * 2;
+                }
+                return val;
+            },
+            /**
+             * @function
+             * @name redraw
+             *
+             * Called on zoom and pan
+             */
+            redraw: function() {
+                panner.attr("transform",
+                    "translate(" + d3.event.translate + ") " +
+                    "scale(" + d3.event.scale + ")"
+                );
+            },
+            /**
+             * @function
+             * @name prepareData
+             *
+             * Convert NetJSON NetworkGraph to the data structure consumed by d3
+             *
+             * @param graph {object}
+             */
+            prepareData: function(graph) {
+                var nodesMap = {},
+                    nodes = graph.nodes.slice(), // copy
+                    links = graph.links.slice(), // copy
+                    nodes_length = graph.nodes.length,
+                    links_length = graph.links.length;
+
+                for(var i = 0; i < nodes_length; i++) {
+                    // count how many links every node has
+                    nodes[i].linkCount = 0;
+                    nodesMap[nodes[i].id] = i;
+                }
+                for(var c = 0; c < links_length; c++) {
+                    var sourceIndex = nodesMap[links[c].source],
+                    targetIndex = nodesMap[links[c].target];
+                    // ensure source and target exist
+                    if(!nodes[sourceIndex]) { throw("source '" + links[c].source + "' not found"); }
+                    if(!nodes[targetIndex]) { throw("target '" + links[c].target + "' not found"); }
+                    links[c].source = nodesMap[links[c].source];
+                    links[c].target = nodesMap[links[c].target];
+                    // add link count to both ends
+                    nodes[sourceIndex].linkCount++;
+                    nodes[targetIndex].linkCount++;
+                }
+                return { "nodes": nodes, "links": links };
+            },
+            /**
+             * @function
+             * @name onClickNode
+             *
+             * Called when a node is clicked
+             */
+            onClickNode: function(n) {
+                var overlay = d3.select(".njg-overlay"),
+                    overlayInner = d3.select(".njg-overlay > .njg-inner"),
+                    html = "<p><b>id</b>: " + n.id + "</p>";
+                    if(n.label) { html += "<p><b>label</b>: " + n.label + "</p>"; }
+                    if(n.properties) {
+                        for(var key in n.properties) {
+                            if(!n.properties.hasOwnProperty(key)) { continue; }
+                            html += "<p><b>"+key.replace(/_/g, " ")+"</b>: " + n.properties[key] + "</p>";
+                    }
+                }
+                if(n.linkCount) { html += "<p><b>links</b>: " + n.linkCount + "</p>"; }
+                if(n.local_addresses) {
+                    html += "<p><b>local addresses</b>:<br>" + n.local_addresses.join('<br>') + "</p>";
+                }
+                overlayInner.html(html);
+                overlay.classed("njg-hidden", false);
+                overlay.style("display", "block");
+                // set "open" class to current node
+                removeOpenClass();
+                d3.select(this).classed("njg-open", true);
+            },
+            /**
+             * @function
+             * @name onClickLink
+             *
+             * Called when a node is clicked
+             */
+            onClickLink: function(l) {
+                var overlay = d3.select(".njg-overlay"),
+                    overlayInner = d3.select(".njg-overlay > .njg-inner"),
+                    html = "<p><b>source</b>: " + (l.source.label || l.source.id) + "</p>";
+                    html += "<p><b>target</b>: " + (l.target.label || l.target.id) + "</p>";
+                    html += "<p><b>cost</b>: " + l.cost + "</p>";
+                if(l.properties) {
+                    for(var key in l.properties) {
+                        if(!l.properties.hasOwnProperty(key)) { continue; }
+                        html += "<p><b>"+ key.replace(/_/g, " ") +"</b>: " + l.properties[key] + "</p>";
+                    }
+                }
+                overlayInner.html(html);
+                overlay.classed("njg-hidden", false);
+                overlay.style("display", "block");
+                // set "open" class to current link
+                removeOpenClass();
+                d3.select(this).classed("njg-open", true);
+            }
+        }, opts);
+
+        // init callback
+        opts.onInit(url, opts);
+
+        if(!opts.animationAtStart) {
+            opts.linkStrength = 2;
+            opts.friction = 0.3;
+            opts.gravity = 0;
+        }
+        if(opts.el == "body") {
+            var body = d3.select(opts.el),
+                rect = body.node().getBoundingClientRect();
+            if (d3._pxToNumber(d3.select("body").style("height")) < 60) {
+                body.style("height", d3._windowHeight() - rect.top - rect.bottom + "px");
+            }
+        }
+        var el = d3.select(opts.el).style("position", "relative"),
+            width = d3._pxToNumber(el.style('width')),
+            height = d3._pxToNumber(el.style('height')),
+            force = d3.layout.force()
+                      .charge(opts.charge)
+                      .linkStrength(opts.linkStrength)
+                      .linkDistance(opts.linkDistanceFunc)
+                      .friction(opts.friction)
+                      .chargeDistance(opts.chargeDistance)
+                      .theta(opts.theta)
+                      .gravity(opts.gravity)
+                      // width is easy to get, if height is 0 take the height of the body
+                      .size([width, height]),
+            zoom = d3.behavior.zoom().scaleExtent(opts.scaleExtent),
+            // panner is the element that allows zooming and panning
+            panner = el.append("svg")
+                       .attr("width", width)
+                       .attr("height", height)
+                       .call(zoom.on("zoom", opts.redraw))
+                       .append("g")
+                       .style("position", "absolute"),
+            svg = d3.select(opts.el + " svg"),
+            drag = force.drag(),
+            overlay = d3.select(opts.el).append("div").attr("class", "njg-overlay"),
+            closeOverlay = overlay.append("a").attr("class", "njg-close"),
+            overlayInner = overlay.append("div").attr("class", "njg-inner"),
+            metadata = d3.select(opts.el).append("div").attr("class", "njg-metadata"),
+            metadataInner = metadata.append("div").attr("class", "njg-inner"),
+            closeMetadata = metadata.append("a").attr("class", "njg-close"),
+            // container of ungrouped networks
+            str = [],
+            selected = [],
+            /**
+             * @function
+             * @name removeOpenClass
+             *
+             * Remove open classes from nodes and links
+             */
+            removeOpenClass = function () {
+                d3.selectAll("svg .njg-open").classed("njg-open", false);
+            };
+            processJson = function(graph) {
+                /**
+                 * Init netJsonGraph
+                 */
+                init = function(url, opts) {
+                    d3.netJsonGraph(url, opts);
+                };
+                /**
+                 * Remove all instances
+                 */
+                destroy = function() {
+                    force.stop();
+                    d3.select("#selectGroup").remove();
+                    d3.select(".njg-overlay").remove();
+                    d3.select(".njg-metadata").remove();
+                    overlay.remove();
+                    overlayInner.remove();
+                    metadata.remove();
+                    svg.remove();
+                    node.remove();
+                    link.remove();
+                    nodes = [];
+                    links = [];
+                };
+                /**
+                 * Destroy and e-init all instances
+                 * @return {[type]} [description]
+                 */
+                reInit = function() {
+                    destroy();
+                    init(url, opts);
+                };
+
+                var data = opts.prepareData(graph),
+                    links = data.links,
+                    nodes = data.nodes;
+
+                // disable some transitions while dragging
+                drag.on('dragstart', function(n){
+                    d3.event.sourceEvent.stopPropagation();
+                    zoom.on('zoom', null);
+                })
+                // re-enable transitions when dragging stops
+                .on('dragend', function(n){
+                    zoom.on('zoom', opts.redraw);
+                })
+                .on("drag", function(d) {
+                    // avoid pan & drag conflict
+                    d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
+                });
+
+                force.nodes(nodes).links(links).start();
+
+                var link = panner.selectAll(".link")
+                                 .data(links)
+                                 .enter().append("line")
+                                 .attr("class", function (link) {
+                                     var baseClass = "njg-link",
+                                         addClass = null;
+                                         value = link.properties && link.properties[opts.linkClassProperty];
+                                     if (opts.linkClassProperty && value) {
+                                         // if value is stirng use that as class
+                                         if (typeof(value) === "string") {
+                                             addClass = value;
+                                         }
+                                         else if (typeof(value) === "number") {
+                                             addClass = opts.linkClassProperty + value;
+                                         }
+                                         else if (value === true) {
+                                             addClass = opts.linkClassProperty;
+                                         }
+                                         return baseClass + " " + addClass;
+                                     }
+                                     return baseClass;
+                                 })
+                                 .on("click", opts.onClickLink),
+                    groups = panner.selectAll(".node")
+                                   .data(nodes)
+                                   .enter()
+                                   .append("g");
+                    node = groups.append("circle")
+                                 .attr("class", function (node) {
+                                     var baseClass = "njg-node",
+                                         addClass = null;
+                                         value = node.properties && node.properties[opts.nodeClassProperty];
+                                     if (opts.nodeClassProperty && value) {
+                                         // if value is stirng use that as class
+                                         if (typeof(value) === "string") {
+                                             addClass = value;
+                                         }
+                                         else if (typeof(value) === "number") {
+                                             addClass = opts.nodeClassProperty + value;
+                                         }
+                                         else if (value === true) {
+                                             addClass = opts.nodeClassProperty;
+                                         }
+                                         return baseClass + " " + addClass;
+                                     }
+                                     return baseClass;
+                                 })
+                                 .attr("r", opts.circleRadius)
+                                 .on("click", opts.onClickNode)
+                                 .call(drag);
+
+                    var labels = groups.append('text')
+                                       .text(function(n){ return n.label || n.id })
+                                       .attr('dx', opts.labelDx)
+                                       .attr('dy', opts.labelDy)
+                                       .attr('class', 'njg-tooltip');
+
+                // Close overlay
+                closeOverlay.on("click", function() {
+                    removeOpenClass();
+                    overlay.classed("njg-hidden", true);
+                });
+                // Close Metadata panel
+                closeMetadata.on("click", function() {
+                    // Reinitialize the page
+                    if(graph.type === "NetworkCollection") {
+                        reInit();
+                    }
+                    else {
+                        removeOpenClass();
+                        metadata.classed("njg-hidden", true);
+                    }
+                });
+                // default style
+                // TODO: probably change defaultStyle
+                // into something else
+                if(opts.defaultStyle) {
+                    var colors = d3.scale.category20c();
+                    node.style({
+                        "fill": function(d){ return colors(d.linkCount); },
+                        "cursor": "pointer"
+                    });
+                }
+                // Metadata style
+                if(opts.metadata) {
+                    metadata.attr("class", "njg-metadata").style("display", "block");
+                }
+
+                var attrs = ["protocol",
+                             "version",
+                             "revision",
+                             "metric",
+                             "router_id",
+                             "topology_id"],
+                    html = "";
+                if(graph.label) {
+                    html += "<h3>" + graph.label + "</h3>";
+                }
+                for(var i in attrs) {
+                    var attr = attrs[i];
+                    if(graph[attr]) {
+                        html += "<p><b>" + attr + "</b>: <span>" + graph[attr] + "</span></p>";
+                    }
+                }
+                // Add nodes and links count
+                html += "<p><b>nodes</b>: <span>" + graph.nodes.length + "</span></p>";
+                html += "<p><b>links</b>: <span>" + graph.links.length + "</span></p>";
+                metadataInner.html(html);
+                metadata.classed("njg-hidden", false);
+
+                // onLoad callback
+                opts.onLoad(url, opts);
+
+                force.on("tick", function() {
+                    link.attr("x1", function(d) {
+                        return d.source.x;
+                    })
+                    .attr("y1", function(d) {
+                        return d.source.y;
+                    })
+                    .attr("x2", function(d) {
+                        return d.target.x;
+                    })
+                    .attr("y2", function(d) {
+                        return d.target.y;
+                    });
+
+                    node.attr("cx", function(d) {
+                        return d.x;
+                    })
+                    .attr("cy", function(d) {
+                        return d.y;
+                    });
+
+                    labels.attr("transform", function(d) {
+                        return "translate(" + d.x + "," + d.y + ")";
+                    });
+                })
+                .on("end", function(){
+                    force.stop();
+                    // onEnd callback
+                    opts.onEnd(url, opts);
+                });
+
+                return force;
+            };
+
+        if(typeof(url) === "object") {
+            processJson(url);
+        }
+        else {
+            /**
+            * Parse the provided json file
+            * and call processJson() function
+            *
+            * @param  {string}     url         The provided json file
+            * @param  {function}   error
+            */
+            d3.json(url, function(error, graph) {
+                if(error) { throw error; }
+                /**
+                * Check if the json contains a NetworkCollection
+                */
+                if(graph.type === "NetworkCollection") {
+                    var selectGroup = body.append("div").attr("id", "njg-select-group"),
+                        select = selectGroup.append("select")
+                                            .attr("id", "select");
+                        str = graph;
+                    select.append("option")
+                          .attr({
+                              "value": "",
+                              "selected": "selected",
+                              "name": "default",
+                              "disabled": "disabled"
+                          })
+                          .html("Choose the network to display");
+                    graph.collection.forEach(function(structure) {
+                        select.append("option").attr("value", structure.type).html(structure.type);
+                        // Collect each network json structure
+                        selected[structure.type] = structure;
+                    });
+                    select.on("change", function() {
+                        selectGroup.attr("class", "njg-hidden");
+                        // Call selected json structure
+                        processJson(selected[this.options[this.selectedIndex].value]);
+                    });
+                }
+                else {
+                    processJson(graph);
+                }
+            });
+        }
+     };
+})();