Source: jdosa_overView.js

"use strict";

/**
 * used to handle everything related to the overview
 * handles Sigma instances and their interplay
 */
class OverView {

    /**
     * creates a new over view with given settings
     */
    constructor() {
        this.selectionCanvas = undefined;
        this.sigInst = undefined;
        this.idxNodeMap = [];
        this.cnt = 0;
        this.min_val = 0;
        this.max_val = 500;
        this.x_axis = undefined;
        this.y_axis = undefined;
        this.activateEdgeLabels = false;
        this.nodeSuffix = '_node';
        this.currentChart = 'SCHEDULED_DEPARTURE';
        this.activeNode = null;
    }
    
    /**
     * create new sigma instance
     * at startup do not show edges do prevent cluttering
     *
     * @param container_id id of div where detail graph should be displayed
     * @param panel_id id of the bootstrap panel wrapping the container
     * @param use_web_gl if true, webgl is used for rendering (keep in mind that less functionality is supported)
     */
    initSigma(container_id, panel_id, use_web_gl) {
        console.log("init overview sigma");
        let _self = this;

        if (!use_web_gl) {
            sigma.canvas.edges.def = sigma.canvas.edges.curvedArrow;
        }

        this.sigInst = new sigma({
            container:container_id,
            renderer: {
                container:document.getElementById(container_id),
                type: (use_web_gl ? 'webgl' : 'canvas')
            },
            settings: {
                minNodeSize: 40,
                maxNodeSize: 40,
                minEdgeSize: 1,
                maxEdgeSize: 20,
                minArrowSize: 4,
                zoomMin: 0.01,
                autoRescale: true,
                hideEdgesOnMove: false,
                edgesPowRatio: 1,
                edgeLabelSize: 'proportional',

            }
        });

        var dragListener = sigma.plugins.dragNodes(this.sigInst, this.sigInst.renderers[0]);

        dragListener.bind('startdrag', function(event) {
          console.log(event);
        });
        dragListener.bind('drag', function(event) {
          console.log(event);
        });
        dragListener.bind('drop', function(event) {
          console.log(event);
        });
        dragListener.bind('dragend', function(event) {
          console.log(event);
        });

        this.sigInst.bind('clickNode', function(event) {
            var node = event.data.node;
            _self.activeNode=node;
            _self.drawChart(node.edges, node.color);
        });

        CustomShapes.init(this.sigInst);
        // hide/show detail view on collapse click (fix for sigma.js)
        $("#" + panel_id + " .collapse-link").click(function(){ $("#" + container_id).toggle();});

        // hide/show unmapped edges on eye click
        $("#" + panel_id + " .edge_toggle").click(function() {
            $("#" + panel_id + " .edge_toggle > i").toggleClass('fa-eye fa-eye-slash');
            _self.activateEdgeLabels = !_self.activateEdgeLabels;
            _self.toggleEdgeLabels(_self.activateEdgeLabels);
            _self.sigInst.refresh();
        });

        // key binder
        $(document).keydown(function(e) {
            e.preventDefault(); // prevent the default action (scroll / move caret)
        });
    }

    /**
     * inits overview box toggle
     * enables users to switch between histogram attributes
     * @param edge edge used to check available attributes
     */
    initHistAttrSwitcher(edge){
        // get all keys
        let keys = Object.keys(edge);
        let _self = this;

        keys.forEach(function(key){
            if (key[0]===key[0].toUpperCase()) {
                if (key==="SCHEDULED_DEPARTURE" || key==="DEPARTURE_TIME" || key==="DEPARTURE_DELAYS" || key==="TAXI_OUT" || key==="WHEELS_OFF" ||
                    key === "SCHEDULED_TIME" || key==="ELAPSED_TIME" || key==="AIR_TIME" || key==="DISTANCE" || key==="WHEELS_ON" || key==="TAXI_IN" || 
                    key==="SCHEDULED_ARRIVAL" || key==="ARRIVAL_TIME"|| key==="ARRIVAL_DELAY"|| key==="AIR_SYSTEM_DELAY" || key==="SECURITY_DELAY" || 
                    key==="AIRLINE_DELAY" || key==="LATE_AIRCRAFT_DELAY"|| key==="WEATHER_DELAY") {

                    $("#hist_attr_selector ul").append('<li><a id="h_sel_' + key +'">' + key + '</a></li>');
                    
                    $("#h_sel_" + key).on('click', function (event) {
                        $("#h_sel_" + key).addClass("active");
                        $("#h_sel_" + _self.currentChart).removeClass("active");
                        _self.currentChart = key;
                        if (_self.activeNode!=null) {
                            _self.drawChart(_self.activeNode.edges, _self.activeNode.color);
                        }
                    });
                }
            }
        });

        // set currently active selection
        $("#h_sel_" + _self.currentChart).addClass("active");
    }

    /**
     * Extracts relevant data of a json-array with the same attributes in each array-element
     * @param key The key of the data, that is extracted (e.g. LONGITUDE, LATITUDE, ...)
     * @param data The input data
     * @return the extracted data
     */
    prepareJsonData(key, data) {
        //var retData = {};
        var itemData = {"items" : []};
        itemData.min=null;
        itemData.max=null;
        itemData.offset=0;

        for (var v in data) {
            for (var w in data[v]) {
                if (w===key) {
                    itemData.items.push(''+data[v][w]);
                    if (itemData.min === null) {
                        itemData.min = data[v][w];
                    }
                    if (itemData.max === null) {
                        itemData.max = data[v][w];
                    }
                    if (data[v][w] < itemData.min) {
                        itemData.min = data[v][w];
                    }
                    if (data[v][w] > itemData.max) {
                        itemData.max = data[v][w];
                    }
                }
            }

        }
        if (itemData.min < 0) {
            itemData.offset = Math.ceil(Math.abs(itemData.min));
        }
        return itemData;
    }

    drawChart(edges, color) {
        var arr = this.prepareJsonData(this.currentChart, edges);
        var histGenerator = d3.histogram().domain([arr.min,arr.max]).thresholds(19);
        
        //console.log(arr);
        var hbins = histGenerator(arr.items);
        var bins = [];
        var _labels = [];
        for (var i = 0; i<hbins.length; i++) {
            bins.push(hbins[i].length);
            var l = hbins[i][0] + '-'+hbins[i][hbins[i].length-1];
            if (l=='undefined-undefined')
                l = '';
            _labels.push(l);
        }
        //console.log(bins);

        var context = document.getElementById('myChart');
        var myChart = new Chart(context, {
            type: 'bar',
            data: {
                labels: _labels,
                datasets: [{
                    label: this.currentChart,
                    data: bins,
                    borderWidth: 0.1,
                    backgroundColor:color
                }]
            },
            options: {
                scales: {
                    yAxes: [{
                        ticks: {
                            beginAtZero:true
                        }
                    }]
                }
            }
        });
    }

    /**
     * Toggles the edge labels
     * @param flag indicates if edge labels shall be drawn.
     */
    toggleEdgeLabels(flag) {
        let _self = this;
        _self.sigInst.graph.edges().forEach(function(edge) {
            if (flag) {
                edge.label = edge.hlabel;
            } else {
                edge.label = '';
            }
        });
    }

    /**
     * positions sigma camera to the center of our scale
     * and removes scaling
     */
    setCameraToCenter() {
        let center_pos = (this.max_val - this.min_val) / 2;
        this.sigInst.camera.goTo({x:center_pos, y:center_pos, ratio:1});
    }

    /**
     * Returns the node id behind the mapping index
     * @param idx the filter index (mapping index)
     */
    getNodeId(idx) {
        return this.idxNodeMap[idx].id;
    }
    /**
     * Returns the node id behind the mapping index
     * @param idx the filter index (mapping index)
     */
    getNodePosition(idx) {
        return this.idxNodeMap[idx].pos;
    }

    /**
     * Adds a groupedNode to the canvas, which consists of nodes and edges
     * that are aggregated in a single node. Edges are not considered when
     * then node is initially added.
     * @param groupedNode The node that is added. Precondition: id, markingColor, nodes and edges must be set.
     */
    addNode(groupedNode) {        
        let _self = this;
        let i = this.cnt++;
        let id = i+_self.nodeSuffix;

        let node = new Object();
        node.id = id;

        node = _self.calculateNodePosition(node);
        node.size = _self.sigInst.settings("minNodeSize");
        node.type = 'square';
        node.color = groupedNode.markingColor;
        node.edges = groupedNode.edges;
        
        _self.idxNodeMap.push({'id': id, 'pos':_self.sigInst.graph.nodes().length});
        _self.sigInst.graph.addNode(node);
        //_self.updateEdges(node);

        //_self.drawChart();
        
        _self.sigInst.refresh();
        //_self.setCameraToCenter();
    }

    /**
     * Removes a node from the canvas.
     * @param idx the idx of the detailView filter
     */
    removeNode(idx) {
        let _self = this;
        let nodepos = _self.getNodePosition(idx);
        for (var i = 0; i < _self.idxNodeMap.length; i++) {
            if (_self.idxNodeMap[i].pos >= nodepos) {
                _self.idxNodeMap[i].pos = _self.idxNodeMap[i].pos-1;
            }
        }
        _self.sigInst.graph.dropNode(_self.getNodeId(idx));

        _self.idxNodeMap.splice(idx, 1);
        _self.sigInst.refresh();
    }

    /**
     * Updates the inner edges and nodes of an overview node.
     * @param idx the idx of the detailView-filter
     * @param the inner nodes
     * @param the outer nodes
     */
    updateNodeEdges(idx, nodes, edges) {
        let _self = this;
        var i;
        for (i=0; i<_self.sigInst.graph.nodes().length;i++) {
            var node = _self.sigInst.graph.nodes()[i];
            if (node.id === _self.getNodeId(idx)) {
                node.nodes = nodes;
                node.edges = edges;
                _self.updateEdges(node);
                if (_self.activeNode==node) {
                    _self.drawChart(_self.activeNode.edges, _self.activeNode.color);
                }
                break;
            }
        }
    }

    /**
     * Updates and draws edges to the canvas
     * @param newNode the node whichs edges are to be drawn.
     */
    updateEdges(newNode) {
        let _self = this;
        if (_self.idxNodeMap.length>=1) {
            _self.sigInst.graph.nodes().forEach(function(outerNode) {
                var newedge = new Object();
                newedge.id = outerNode.id+newNode.id;
                newedge.source = outerNode.id;
                newedge.target = newNode.id;
                
                newedge.color = outerNode.color;
                var update = false;
                var i;
                for (i=0; i<_self.sigInst.graph.edges().length;i++) {
                    var outerEdge = _self.sigInst.graph.edges()[i];
                    if (outerEdge.id==newedge.id) {
                        newedge = outerEdge;
                        update=true;
                        break;
                    }
                }
                
                newedge.size = 0;
                if (newNode.nodes!== undefined) {
                    newNode.nodes.forEach(function(n2) {
                        outerNode.edges.forEach(function(e) {
                            if (e.target===n2.id)
                                newedge.size = newedge.size+1;
                        });
                    });
                }
                var countedsize = newedge.size;
                
                newedge.hlabel = ''+countedsize; 
                if (_self.activateEdgeLabels) {
                    newedge.label = newedge.hlabel;
                }

                newedge.type = 'curvedArrow';
                newedge.size=(newedge.size/100)+1;
                if (update==false){
                    _self.sigInst.graph.addEdge(newedge);
                }
                if (countedsize == 0){
                    _self.sigInst.graph.dropEdge(newedge.id);
                }

            });

            _self.sigInst.graph.nodes().forEach(function(outerNode) {
                var newedge = new Object();
                newedge.id = newNode.id+outerNode.id;
                newedge.source =  newNode.id;
                newedge.target = outerNode.id;
                
                newedge.color = newNode.color;
                var update = false;
                var i;
                for (i=0; i<_self.sigInst.graph.edges().length;i++) {
                    var outerEdge = _self.sigInst.graph.edges()[i];
                    if (outerEdge.id==newedge.id) {
                        newedge = outerEdge;
                        update=true;
                        break;
                    }
                }

                newedge.size = 0;

                if (outerNode.nodes!== undefined) {
                    outerNode.nodes.forEach(function(n2) {
                        newNode.edges.forEach(function(e) {
                            if (e.target===n2.id)
                                newedge.size = newedge.size+1;
                        });
                    });
                }

                var countedsize = newedge.size;

                newedge.hlabel = ''+countedsize; 
                if (_self.activateEdgeLabels) {
                    newedge.label = newedge.hlabel;
                }

                newedge.size=(newedge.size/100)+1;
                newedge.type = 'curvedArrow';
                if (update==false){
                    _self.sigInst.graph.addEdge(newedge);
                }

                if (countedsize == 0){
                    _self.sigInst.graph.dropEdge(newedge.id);
                }

            });


        }
        _self.sigInst.refresh();
    }

    updateColor(idx, color) {
        let _self = this;
        
        var node = _self.sigInst.graph.nodes()[_self.getNodePosition(idx)];
        node.color = color;

        _self.sigInst.graph.edges().forEach(function(edge) {
            if (edge.source===node.id) {
                edge.color = color;
            }
        });
        if (_self.activeNode!=null) {
            _self.drawChart(_self.activeNode.edges, _self.activeNode.color);
        }
        _self.sigInst.refresh();
    }

    /**
     * switches the filter idx, that are used to map to the node ids and their positions
     * @param filterIdx the old filterIdx
     * @param newIdx the new filterIdx
     */
    switchFilterIdx(filterIdx,newIdx) {
        let oldval = this.idxNodeMap[filterIdx];
        this.idxNodeMap[filterIdx] = this.idxNodeMap[newIdx];
        this.idxNodeMap[newIdx] = oldval;
    }

    /**
     * calculates a simple node positions for the sigma nodes
     * the node is not yet pushed to graph.nodes() or the mapping array
     * @param node The node the positioning is set for
     * @returns the node with calculated .x and .y
     */
    calculateNodePosition(node) {
        let _self = this;
        
        var nodeDistance = _self.sigInst.settings("minNodeSize");
        if (_self.idxNodeMap.length==0) {
            node.x = (_self.max_val/2);// - nodeDistance;
            node.y = (_self.max_val/2);// - nodeDistance;
            return node;
        }
        
        if (_self.cnt%2 == 0) {
            //var prevnode = _self.groupedNodeArr[_self.idxNodeMap.length-2];
            var sigmanode = _self.sigInst.graph.nodes()[_self.idxNodeMap.length-1];
            sigmanode.x = (_self.max_val/2) - nodeDistance; 

            node.x = (sigmanode.x) + nodeDistance*2;
            node.y = sigmanode.y;
            return node;
        } else {
            //var prevnode = _self.groupedNodeArr[_self.groupedNodeArr.length-3];
            var i = Math.max(_self.sigInst.graph.nodes().length-2,0);
            //console.log("i: "+i);
            var sigmanode = _self.sigInst.graph.nodes()[i];

            node.x = (_self.max_val/2);
            node.y = sigmanode.y+nodeDistance*2;
            return node;
        }
    }
};