<commit_before>/*
 * Copyright (C) 2016 Language Technology Group and Interactive Graphics Systems Group, Technische Universität Darmstadt, Germany
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

define([
    'angular',
    'ngMaterial',
    'ngVis',
    'angularMoment'
], function(angular) {
    'use strict';

    angular.module('myApp.network', ['ngMaterial', 'ngVis', 'play.routing', 'angularMoment']);
    angular.module('myApp.network')
        // TODO This factory is crap, but referenced in the code ... remove it!
        // This factory is used to share graph properties between this module and the app.js
        .factory('graphPropertiesShareService', function () {
            var graphProperties = {
                // Order: locations, organizations, persons, miscellaneous
                categoryColors: ["#8dd3c7", "#fb8072","#bebada", "#ffffb3"],
                categories:
                    [
                        {id: 'LOC', full: 'Location'},
                        {id: 'ORG', full: 'Organization'},
                        {id: 'PER', full: 'Person'},
                        {id: 'MISC', full: 'Miscellaneous'}
                    ],
                CATEGORY_COLOR_INDEX_LOC: 0,
                CATEGORY_COLOR_INDEX_ORG: 1,
                CATEGORY_COLOR_INDEX_PER: 2,
                CATEGORY_COLOR_INDEX_MISC: 3,
                // Delivers the index of a given category name
                getIndexOfCategory: function (category) {
                    switch (category) {
                        case 'LOC':
                            return this.CATEGORY_COLOR_INDEX_LOC;
                        case 'ORG':
                            return this.CATEGORY_COLOR_INDEX_ORG;
                        case 'PER':
                            return this.CATEGORY_COLOR_INDEX_PER;
                        default:
                            return this.CATEGORY_COLOR_INDEX_MISC;
                    }
                }
            };
            return graphProperties;
        })
        // Network Legend Controller
        .controller('LegendController', ['$scope', '$timeout', 'VisDataSet', 'graphProperties', function ($scope, $timeout, VisDataSet, graphProperties) {

            $scope.legendNodes = new VisDataSet([]);
            this.legendOptions = graphProperties.legendOptions;
            this.legendData = { nodes: $scope.legendNodes, edges: [] };
            this.legendEvents = { "onload": function(network) { $scope.legendNetwork = network; } };

            $scope.addLegend = function() {
                var x = 0; var y = 0; var distance = 100;
                $scope.legendNodes.add([
                    { id: -1, x: x, y: y, label: 'Per', group: 0, value: 1, fixed: true, physics: false },
                    { id: -2, x: x + distance, y: y, label: 'Org', group: 1, value: 1, fixed: true, physics: false },
                    { id: -3, x: x + 2*distance, y: y, label: 'Loc', group: 2, value: 1, fixed: true, physics: false },
                    { id: -4, x: x + 3*distance, y: y, label: 'Misc', group: 3, value: 1, fixed: true, physics: false }
                ]);
                $scope.legendNetwork.fit();
            };

            // Add nodes after the legend div is added to the dom
            $timeout(function(){
                $scope.addLegend();
            });
        }])
        // Network Controller
        .controller('FancyNetworkController', ['$scope', '$timeout', '$compile', '$mdDialog', 'VisDataSet', 'playRoutes', 'ObserverService', '_', 'physicOptions', 'graphProperties', function ($scope, $timeout, $compile, $mdDialog, VisDataSet, playRoutes, ObserverService, _, physicOptions, graphProperties) {

            var self = this;

            /* Entity types to their id */
            self.types = {};

            /* Background collection */
            self.nodes = new VisDataSet([]);
            self.edges = new VisDataSet([]);

            /* Graph collection filtered during runtime */
            self.nodesDataset = new VisDataSet([]);
            self.edgesDataset = new VisDataSet([]);

            self.physicOptions = physicOptions;


            self.nodeMenu = [
                {
                    title: 'Add as filter',
                    action: function(value, nodeId) { addNodeFilter(nodeId); }
                }, {
                    title: 'Edit node',
                    action: function(value, nodeId) { editNode(nodeId); }
                }, {
                    title: 'Expand',
                    action: function(value, nodeId) { expandNode(nodeId); }
                },
                {
                    title: 'Hide',
                    action: function(value, nodeId) { hideNode(nodeId); }
                }, {
                    title: 'Blacklist',
                    action: function(value, nodeId) { blacklistNode(nodeId); }
                }
            ];

            self.edgeMenu = [{
                title: 'Add as filter',
                action: function(value, edgeId) { addEdgeFilter(edgeId); }
            }];

            $scope.observerService = ObserverService;

            // TODO: would be cool to have the event type as argument i.e. remove or add
            $scope.observer_subscribe_fulltext = function(items) {
                // TODO: check why map does not work here
                $scope.fulltextFilters = items;//items.map(function(f) { return f.data.name; });
            };
            $scope.observer_subscribe_metadata = function(items) { $scope.metadataFilters = items};
            $scope.observer_subscribe_entity = function(items) { $scope.entityFilters = items};
            $scope.observerService.subscribeItems($scope.observer_subscribe_fulltext, "fulltext");
            $scope.observerService.subscribeItems($scope.observer_subscribe_metadata, "metadata");
            $scope.observerService.subscribeItems($scope.observer_subscribe_entity, "entity");

            function currentFilter() {
                var fulltext = $scope.fulltextFilters.map(function(v) { return v.data.name; });
                var entities = $scope.entityFilters.map(function(v) { return v.data.id; });
                var timeRange = $scope.observerService.getTimeRange();
                var facets = $scope.observerService.getFacets();

                return { fulltext: fulltext, entities: entities, timeRange: timeRange, facets: facets };
            }

            $scope.graphOptions = graphProperties.options;
            $scope.graphEvents = {
                "startStabilizing": stabilizationStart,
                "stabilized": stabilized,
                "stabilizationIterationsDone": stabilizationDone,
                "onload": onNetworkLoad,
                "dragEnd": dragNodeDone,
                "oncontext": onContext,
                "click": clickEvent,
                "dragging": dragEvent,
                "hoverEdge": hoverEdge,
                "hoverNode": hoverNode
            };

            /* Current value of the edge significance slider */
            $scope.edgeImportance = 1;
            /* Maximum edge value of the current underlying graph collection. Updated in reload method */
            $scope.maxEdgeImportance = 80000;
            /* */
            $scope.numPer = 5;
            $scope.numOrg = 5;
            $scope.numLoc = 5;
            $scope.numMisc = 5;

            /* Indicates whether the network is initialized or new data is being loaded */
            $scope.loading = true;

            $scope.$watch('edgeImportance', handleEdgeSlider);

            $scope.reloadGraph = function() {
                var filters = currentFilter();

                 var fraction = [
                    {"key": "PER", "data": $scope.numPer },
                    {"key": "ORG", "data": $scope.numOrg },
                    {"key": "LOC", "data": $scope.numLoc },
                    {"key": "MISC", "data": $scope.numMisc }
                ];

                playRoutes.controllers.NetworkController.induceSubgraph(filters.fulltext, filters.facets, filters.entities, filters.timeRange, fraction, []).get().then(function(response) {
                        // Enable physics for new graph data when network is initialized
                        if(!_.isUndefined(self.network)) {
                            applyPhysicsOptions(self.physicOptions);
                        }
                        $scope.loading = true;

                        // Assignment from entity types to their id for coloring
                        self.types = response.data.types;

                        //var originalMax = _.max(response.data.entities, function(n) { return n.count; }).count;
                        //var originalMin = _.min(response.data.entities, function(n) { return n.count; }).count;

                        var nodes = response.data.entities.map(function(n) {
                            // See css property div.network-tooltip for custom tooltip styling
                            // map counts to interval [1,2] for nodes mass
                            /*var mass = ((n.count - originalMin) / (originalMax - originalMin)) * (2 - 1) + 1;
                            // If all nodes have the same occurrence assign same mass. This also prevents errors
                            // for wrong interval mappings e.g. [1,1] to [1,2] yields NaN for the mass.
                            if(originalMin == originalMax) mass = 1;*/
                            return { id: n.id, label: n.label, type: n.type,  value: n.count, group: n.group };
                        });

                        self.nodesDataset.clear();
                        self.nodesDataset.add(nodes);
                        // Highlight new nodes after each filtering step
                        markNewNodes(nodes.map(function(n) { return n.id; }));
                        self.nodes.clear();

                        var edges = response.data.relations.map(function(n) {
                            return {from: n[0], to: n[1], value: n[2] };
                        });

                        self.edges.clear();
                        self.edgesDataset.clear();
                        self.edgesDataset.add(edges);

                        // Update the maximum edge importance slider value
                        $scope.maxEdgeImportance = (self.edgesDataset.length > 0) ? self.edgesDataset.max("value").value : 0;
                        console.log("" + self.nodesDataset.length + " nodes loaded");

                        // Initialize the graph
                        $scope.graphData = {
                            nodes: self.nodesDataset,
                            edges: self.edgesDataset
                        };
                });
            };


            // Initialize graph
            $scope.reloadGraph();

            $scope.observerService.registerObserverCallback(function() {
                console.log("Update network");
                $scope.reloadGraph();
            });

            function applyPhysicsOptions(options) {
                console.log('Physics simulation turned on');
                $scope.graphOptions['physics'] = options;
            }

            function disablePhysics() {
                console.log('Physics simulation turned off');
                $scope.graphOptions['physics'] = false;
            }

            function markNewNodes(nextNodeIds) {
                // Don't highlight all nodes for the initial graph
                if(self.nodes.length > 0) {
                    var currentNodeIds = self.nodes.getIds();
                    var sec = _.intersection(nextNodeIds, currentNodeIds);
                    // Remove old marking from the nodes in order to show only the new nodes for one single step
                    var cleanNodes = self.nodes.get(sec).map(function(n) { return _.extend(n, { 'shapeProperties': { borderDashes: false }, color: undefined, borderWidth: 1})});
                    self.nodes.update(cleanNodes);
                    self.nodesDataset.update(cleanNodes);

                    var diff = _.difference(nextNodeIds, currentNodeIds);
                    // Give new nodes a white and dashed border
                    var modifiedNodes = diff.map(function(id) { return { id: id, shapeProperties: { borderDashes: [5, 5] }, color: { border: 'white' }, borderWidth: 2 }});
                    self.nodesDataset.update(modifiedNodes);

                    // TODO Move
                    // Fix nodes from the previous filtering step. This ensures that the node will always preserve its position.
                    // Also remove hidden state between filtering steps.
                    var fixedNodes = self.nodes.get(sec).map(function(n) { return _.extend(n, { fixed: { x: true, y: true }, hidden: false })});
                    self.nodesDataset.update(fixedNodes);
                }
            }

            function addFullscreenButton() {
                var panel = angular.element(document.getElementsByClassName('vis-navigation')[0]);
                var buttonTemplate = '<div class="vis-button vis-fullscreen-button" ng-click="FancyNetworkController.toggleFullscreen()"></div>';
                var button = $compile(buttonTemplate)($scope);
                panel.append(button);
            }

            function addLegendButton() {
                var panel = angular.element(document.getElementsByClassName('vis-navigation')[0]);
                // Show or hide network legend
                var buttonTemplate = '<div class="vis-button vis-legend-button" ng-click="showLegend = !showLegend"></div>';
                var button = $compile(buttonTemplate)($scope);
                panel.append(button);
            }

            // ----------------------------------------------------------------------------------<commit_msg>Fix issue causing wrong node size after filerting<commit_after>/*
 * Copyright (C) 2016 Language Technology Group and Interactive Graphics Systems Group, Technische Universität Darmstadt, Germany
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

define([
    'angular',
    'ngMaterial',
    'ngVis',
    'angularMoment'
], function(angular) {
    'use strict';

    angular.module('myApp.network', ['ngMaterial', 'ngVis', 'play.routing', 'angularMoment']);
    angular.module('myApp.network')
        // TODO This factory is crap, but referenced in the code ... remove it!
        // This factory is used to share graph properties between this module and the app.js
        .factory('graphPropertiesShareService', function () {
            var graphProperties = {
                // Order: locations, organizations, persons, miscellaneous
                categoryColors: ["#8dd3c7", "#fb8072","#bebada", "#ffffb3"],
                categories:
                    [
                        {id: 'LOC', full: 'Location'},
                        {id: 'ORG', full: 'Organization'},
                        {id: 'PER', full: 'Person'},
                        {id: 'MISC', full: 'Miscellaneous'}
                    ],
                CATEGORY_COLOR_INDEX_LOC: 0,
                CATEGORY_COLOR_INDEX_ORG: 1,
                CATEGORY_COLOR_INDEX_PER: 2,
                CATEGORY_COLOR_INDEX_MISC: 3,
                // Delivers the index of a given category name
                getIndexOfCategory: function (category) {
                    switch (category) {
                        case 'LOC':
                            return this.CATEGORY_COLOR_INDEX_LOC;
                        case 'ORG':
                            return this.CATEGORY_COLOR_INDEX_ORG;
                        case 'PER':
                            return this.CATEGORY_COLOR_INDEX_PER;
                        default:
                            return this.CATEGORY_COLOR_INDEX_MISC;
                    }
                }
            };
            return graphProperties;
        })
        // Network Legend Controller
        .controller('LegendController', ['$scope', '$timeout', 'VisDataSet', 'graphProperties', function ($scope, $timeout, VisDataSet, graphProperties) {

            $scope.legendNodes = new VisDataSet([]);
            this.legendOptions = graphProperties.legendOptions;
            this.legendData = { nodes: $scope.legendNodes, edges: [] };
            this.legendEvents = { "onload": function(network) { $scope.legendNetwork = network; } };

            $scope.addLegend = function() {
                var x = 0; var y = 0; var distance = 100;
                $scope.legendNodes.add([
                    { id: -1, x: x, y: y, label: 'Per', group: 0, value: 1, fixed: true, physics: false },
                    { id: -2, x: x + distance, y: y, label: 'Org', group: 1, value: 1, fixed: true, physics: false },
                    { id: -3, x: x + 2*distance, y: y, label: 'Loc', group: 2, value: 1, fixed: true, physics: false },
                    { id: -4, x: x + 3*distance, y: y, label: 'Misc', group: 3, value: 1, fixed: true, physics: false }
                ]);
                $scope.legendNetwork.fit();
            };

            // Add nodes after the legend div is added to the dom
            $timeout(function(){
                $scope.addLegend();
            });
        }])
        // Network Controller
        .controller('FancyNetworkController', ['$scope', '$timeout', '$compile', '$mdDialog', 'VisDataSet', 'playRoutes', 'ObserverService', '_', 'physicOptions', 'graphProperties', function ($scope, $timeout, $compile, $mdDialog, VisDataSet, playRoutes, ObserverService, _, physicOptions, graphProperties) {

            var self = this;

            /* Entity types to their id */
            self.types = {};

            /* Background collection */
            self.nodes = new VisDataSet([]);
            self.edges = new VisDataSet([]);

            /* Graph collection filtered during runtime */
            self.nodesDataset = new VisDataSet([]);
            self.edgesDataset = new VisDataSet([]);

            self.physicOptions = physicOptions;


            self.nodeMenu = [
                {
                    title: 'Add as filter',
                    action: function(value, nodeId) { addNodeFilter(nodeId); }
                }, {
                    title: 'Edit node',
                    action: function(value, nodeId) { editNode(nodeId); }
                }, {
                    title: 'Expand',
                    action: function(value, nodeId) { expandNode(nodeId); }
                },
                {
                    title: 'Hide',
                    action: function(value, nodeId) { hideNode(nodeId); }
                }, {
                    title: 'Blacklist',
                    action: function(value, nodeId) { blacklistNode(nodeId); }
                }
            ];

            self.edgeMenu = [{
                title: 'Add as filter',
                action: function(value, edgeId) { addEdgeFilter(edgeId); }
            }];

            $scope.observerService = ObserverService;

            // TODO: would be cool to have the event type as argument i.e. remove or add
            $scope.observer_subscribe_fulltext = function(items) {
                // TODO: check why map does not work here
                $scope.fulltextFilters = items;//items.map(function(f) { return f.data.name; });
            };
            $scope.observer_subscribe_metadata = function(items) { $scope.metadataFilters = items};
            $scope.observer_subscribe_entity = function(items) { $scope.entityFilters = items};
            $scope.observerService.subscribeItems($scope.observer_subscribe_fulltext, "fulltext");
            $scope.observerService.subscribeItems($scope.observer_subscribe_metadata, "metadata");
            $scope.observerService.subscribeItems($scope.observer_subscribe_entity, "entity");

            function currentFilter() {
                var fulltext = $scope.fulltextFilters.map(function(v) { return v.data.name; });
                var entities = $scope.entityFilters.map(function(v) { return v.data.id; });
                var timeRange = $scope.observerService.getTimeRange();
                var facets = $scope.observerService.getFacets();

                return { fulltext: fulltext, entities: entities, timeRange: timeRange, facets: facets };
            }

            $scope.graphOptions = graphProperties.options;
            $scope.graphEvents = {
                "startStabilizing": stabilizationStart,
                "stabilized": stabilized,
                "stabilizationIterationsDone": stabilizationDone,
                "onload": onNetworkLoad,
                "dragEnd": dragNodeDone,
                "oncontext": onContext,
                "click": clickEvent,
                "dragging": dragEvent,
                "hoverEdge": hoverEdge,
                "hoverNode": hoverNode
            };

            /* Current value of the edge significance slider */
            $scope.edgeImportance = 1;
            /* Maximum edge value of the current underlying graph collection. Updated in reload method */
            $scope.maxEdgeImportance = 80000;
            /* */
            $scope.numPer = 5;
            $scope.numOrg = 5;
            $scope.numLoc = 5;
            $scope.numMisc = 5;

            /* Indicates whether the network is initialized or new data is being loaded */
            $scope.loading = true;

            $scope.$watch('edgeImportance', handleEdgeSlider);

            $scope.reloadGraph = function() {
                var filters = currentFilter();

                 var fraction = [
                    {"key": "PER", "data": $scope.numPer },
                    {"key": "ORG", "data": $scope.numOrg },
                    {"key": "LOC", "data": $scope.numLoc },
                    {"key": "MISC", "data": $scope.numMisc }
                ];

                playRoutes.controllers.NetworkController.induceSubgraph(filters.fulltext, filters.facets, filters.entities, filters.timeRange, fraction, []).get().then(function(response) {
                        // Enable physics for new graph data when network is initialized
                        if(!_.isUndefined(self.network)) {
                            applyPhysicsOptions(self.physicOptions);
                        }
                        $scope.loading = true;

                        // Assignment from entity types to their id for coloring
                        self.types = response.data.types;

                        //var originalMax = _.max(response.data.entities, function(n) { return n.count; }).count;
                        //var originalMin = _.min(response.data.entities, function(n) { return n.count; }).count;

                        var nodes = response.data.entities.map(function(n) {
                            // See css property div.network-tooltip for custom tooltip styling
                            // map counts to interval [1,2] for nodes mass
                            /*var mass = ((n.count - originalMin) / (originalMax - originalMin)) * (2 - 1) + 1;
                            // If all nodes have the same occurrence assign same mass. This also prevents errors
                            // for wrong interval mappings e.g. [1,1] to [1,2] yields NaN for the mass.
                            if(originalMin == originalMax) mass = 1;*/
                            return { id: n.id, label: n.label, type: n.type,  value: n.count, group: n.group };
                        });

                        self.nodesDataset.clear();
                        self.nodesDataset.add(nodes);
                        // Highlight new nodes after each filtering step
                        markNewNodes(nodes.map(function(n) { return n.id; }));
                        self.nodes.clear();

                        var edges = response.data.relations.map(function(n) {
                            return {from: n[0], to: n[1], value: n[2] };
                        });

                        self.edges.clear();
                        self.edgesDataset.clear();
                        self.edgesDataset.add(edges);

                        // Update the maximum edge importance slider value
                        $scope.maxEdgeImportance = (self.edgesDataset.length > 0) ? self.edgesDataset.max("value").value : 0;
                        console.log("" + self.nodesDataset.length + " nodes loaded");

                        // Initialize the graph
                        $scope.graphData = {
                            nodes: self.nodesDataset,
                            edges: self.edgesDataset
                        };
                });
            };


            // Initialize graph
            $scope.reloadGraph();

            $scope.observerService.registerObserverCallback(function() {
                console.log("Update network");
                $scope.reloadGraph();
            });

            function applyPhysicsOptions(options) {
                console.log('Physics simulation turned on');
                $scope.graphOptions['physics'] = options;
            }

            function disablePhysics() {
                console.log('Physics simulation turned off');
                $scope.graphOptions['physics'] = false;
            }

            function markNewNodes(nextNodeIds) {
                // Don't highlight all nodes for the initial graph
                if(self.nodes.length > 0) {
                    // This dataset contains the nodes from the previous filtering step without dynamic changing information
                    var previousNodes = new VisDataSet(self.nodes.map(function(n)  { return _.omit(n, 'hidden', 'title', 'value'); }));

                    var currentNodeIds = previousNodes.getIds();
                    var sec = _.intersection(nextNodeIds, currentNodeIds);
                    // Remove old marking from the nodes in order to show only the new nodes for one single step
                    var cleanNodes = previousNodes.get(sec).map(function(n) { return _.extend(n, { 'shapeProperties': { borderDashes: false }, color: undefined, borderWidth: 1 })});
                    previousNodes.update(cleanNodes);
                    self.nodesDataset.update(cleanNodes);

                    var diff = _.difference(nextNodeIds, currentNodeIds);
                    // Give new nodes a white and dashed border
                    var modifiedNodes = diff.map(function(id) { return { id: id, shapeProperties: { borderDashes: [5, 5] }, color: { border: 'white' }, borderWidth: 2 }});
                    self.nodesDataset.update(modifiedNodes);

                    // TODO Move
                    // Fix nodes from the previous filtering step. This ensures that the node will always preserve its position.
                    // Also remove hidden state between filtering steps and (dynamic) tooltip.
                    var fixedNodes = previousNodes.get(sec).map(function(n) { return _.extend(n, { fixed: { x: true, y: true } })});
                    self.nodesDataset.update(fixedNodes);
                }
            }

            function addFullscreenButton() {
                var panel = angular.element(document.getElementsByClassName('vis-navigation')[0]);
                var buttonTemplate = '<div class="vis-button vis-fullscreen-button" ng-click="FancyNetworkController.toggleFullscreen()"></div>';
                var button = $compile(buttonTemplate)($scope);
                panel.append(button);
            }

            function addLegendButton() {
                var panel = angular.element(document.getElementsByClassName('vis-navigation')[0]);
                // Show or hide network legend
                var buttonTemplate = '<div class="vis-button vis-legend-button" ng-click="showLegend = !showLegend"></div>';
                var button = $compile(buttonTemplate)($scope);
                panel.append(button);
            }

            // ----------------------------------------------------------------------------------
{'fraction_non_alphanumeric': 0.11323268206039076, 'fraction_numerical': 0.004645443366580134, 'mean_word_length': 1.5936392629340892, 'pattern_counts': {'":': 38, '<': 13, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1792814', 'n_tokens_mistral': 7374, 'n_tokens_neox': 6880, 'n_words': 1929}