<commit_before>angular.module('app', ['ui.router', 'ui.bootstrap'])
    .constant('API_ROOT_URL', 'http://www.localfinance.fr/api')
    .constant('TILES_ROOT_URL', 'http://{s}.tile.nosfinanceslocales.fr/tiles') // get this info from server ?
    .constant('THUMBNAILS_URL', '/static/thumbnails')
    .constant('TEMPLATE_URL', '/static/templates')
    .factory('mapUtils', function(TILES_ROOT_URL, THUMBNAILS_URL) {
        return {
            getTileUrl: function(map_id) {
                return TILES_ROOT_URL + '/' + map_id + "/{z}/{x}/{y}.png";
            },
            getGridUrl: function(map_id) {
                return TILES_ROOT_URL + '/' + map_id + "/{z}/{x}/{y}.grid.json";
            },
            getThumbnailUrl: function(map_id) {
                return THUMBNAILS_URL + '/' + map_id + '.png';
            }
        };
    })
    .factory('Resource', function($http, API_ROOT_URL) {
        function Factory(name) {
            var resource = {
                get: function(id) {
                    return $http.get(API_ROOT_URL + '/' + name + '/' + id)
                        .then(function(response) {
                            return response.data.results;
                        });
                },
                all: function() {
                    return $http.get(API_ROOT_URL + '/' + name + 's')
                        .then(function(response) {
                            return response.data.results;
                        });
                }
            }
            return resource;
        }
        return Factory
    })
    .factory('CitySearch', function($http, API_ROOT_URL) {
        return {
            get: function(term) {
                return $http.get(API_ROOT_URL + '/' + 'city_search' + '?term=' + term)
                    .then(function(response) {
                        return response.data.results;
                    });
            }
        }
    })
    .config(
        [ '$stateProvider', '$urlRouterProvider', 'TEMPLATE_URL',
        function ($stateProvider, $urlRouterProvider, TEMPLATE_URL) {
            $urlRouterProvider.when('', '/maps/');
            $urlRouterProvider.otherwise('/maps/');
            $stateProvider
                .state('about', {
                    url: '/about',
                    templateUrl: TEMPLATE_URL + '/about.html',
                })
                .state('localfinance', {
                    url: '/{id:[0-9]{1,4}}',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/localfinance.detail.html',
                            controller: 'LocalFinanceDetailCtrl'
                        },
                    }
                })
                .state('maps', {
                    abstract: true,
                    url: '/maps',
                    templateUrl: TEMPLATE_URL + '/maps.html',
                    controller: 'MapsCtrl',
                    resolve: {
                        timemaps: function(Resource) {
                            return Resource('timemap').all()
                        },
                        stats: function(Resource) {
                            return Resource('stat').all();
                        }
                    }
                })
                .state('maps.list', {
                    url: '/',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/map.list.html',
                            controller: 'MapListCtrl'
                        }
                    }
                })
                .state('maps.detail', {
                    url: '/{var_name}',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/map.detail.html',
                            controller: 'MapDetailCtrl'
                        }
                    }
                });
        }])
    .controller('Home', ['$scope', '$state',
        function($scope, $state, stats) {
            $scope.$state = $state;
            $scope.stats = stats;
    }])
    .controller('MapsCtrl', ['$scope', '$state', 'mapUtils', 'timemaps', 'stats',
        function($scope, $state, mapUtils, timemaps, stats) {
            $scope.mapUtils = mapUtils;
            $scope.timemaps = timemaps;
            $scope.stats = stats;
        }])
    .controller('MapListCtrl', ['$scope', '$state', '$timeout',
        function($scope, $state, $timeout) {
            $scope.thumbailUrls = [];
            function fireDigestEverySecond() {
                $timeout(fireDigestEverySecond , 1000);
            };
            fireDigestEverySecond();
        }])
    .controller('MapDetailCtrl', ['$scope', '$stateParams', 'Resource', 'mapUtils', 'CitySearch',
        function($scope, $stateParams, Resource, mapUtils, CitySearch) {
            $scope.getCities = CitySearch.get;
            $scope.centerMapAndLoadStatCity = function(city) {
                // first update map
                $scope.mapOptions = {
                    zoom: 8,
                    lat: city.lat,
                    lng: city.lng
                }

                // then get stats of the city
                $scope.loadCityFinance(city.id);
            };
            var colors = d3.scale.category10();;
            // get the timemap for this variable
            $scope.timemap = $scope.timemaps.filter(function(timemap) {
                return (timemap.var_name == $stateParams.var_name)
            })[0];
            // same for stats
            $scope.linesData = [
                {
                    name: 'FRANCE',
                    data: $scope.stats.filter(function(stat){
                        return (stat.var_name == $stateParams.var_name)
                    })[0].mean_by_year,
                    color: colors('FRANCE')
                }
            ];
            $scope.removeLine = function(id) {
                // make impossible to remove france
                if (id) {
                    $scope.linesData.some(function(d, i) {
                        if (d.id==id){
                            $scope.linesData.splice(i, i);
                            return true;
                        }
                    });
                }
            }
            // take first map
            $scope.year = $scope.timemap.maps[0].year;
            $scope.opacity = 0.8;
            function findMapData(year){
                return $scope.timemap.maps.filter(function(m) {
                    return (m.year == year)
                })[0];
            }
            $scope.mapData = findMapData($scope.timemap.maps[0].year);
            $scope.loadCityFinance = function(id) {
                function checkLine(id) {
                    return ($scope.linesData.filter(function(d){return (id==d.id)}).length > 0)
                }
                if  (checkLine(id)) {
                    return ;
                }
                Resource('finance').get(id).then(function(results) {
                    if (results.length == 0) {
                        console.log("No data");
                        return;
                    }
                    // get var_name stat
                    var res = results.map(function(d){
                        return [d.year, parseFloat(d.data[$scope.timemap.var_name])];
                    })
                    if (!checkLine(id)) {
                        $scope.linesData.push({name: results[0].name, data: res, color: colors(results[0].name), id: id});
                    }
                });
            }
            $scope.onClick = function(data) {
                $scope.loadCityFinance(data.id);
                $scope.$apply();
            }
            $scope.onMouseOver = function(data) {
                $scope.mouseOverdata = data;
                $scope.$apply();
            }
            document.data = $scope.linesData;
        }])
    .directive('leafletMap', function (mapUtils) {
        return {
            restrict: "A",
            replace: false,
            scope: {
                timemap: '=',
                variables: '=',
                options: '=',
                click: '&onClick',
                mouseOver: '&onMouseOver',
                mouseOut: '&onMouseOut'
            },
            link: function($scope, element, attrs, controller) {
                var onClick = $scope.click(),
                    onMouseOver = $scope.mouseOver(),
                    onMouseOut = $scope.mouseOut();
                var refmap = $scope.timemap.maps[0];

                var southWest = L.latLng(refmap.extent[1],
                                         refmap.extent[0]),
                    northEast = L.latLng(refmap.extent[3],
                                         refmap.extent[2]),
                    bounds = L.latLngBounds(southWest, northEast);
                var stamenAttribution = 'Map tiles by Stamen Design, under CC BY 3.0';
                var basemap = new L.TileLayer(
                        "http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png",
                        {attribution: stamenAttribution});
                var layers = [basemap];
                var yearsToLayers = {}, yearsToUtfGrids = {}, currentUtfGrid, years = [];
                var rcAttribution = 'Data freed by Regards Citoyens '
                for(var imap=0;imap<$scope.timemap.maps.length;imap++) {
                    var map = $scope.timemap.maps[imap];
                    var layer = new L.TileLayer(
                        mapUtils.getTileUrl(map.id),
                        {opacity: 0, subdomains: 'abc',
                         attribution: rcAttribution}
                    );
                    layers.push(layer);
                    yearsToLayers[map.year] = layer;
                    yearsToUtfGrids[map.year] = new L.UtfGrid(mapUtils.getGridUrl(map.id), {useJsonP: false});
                    years.push(map.year);
                }

                var interactiveLayerGroup = L.layerGroup(layers);

                var options = {
                    center: L.latLng(46.22587886848165, 2.21040301636657),
                    zoom: 6,
                    minZoom: refmap.minzoom, // should take the max of min
                    maxZoom: refmap.maxzoom, // should take the min of max
                };

                // leaflet map
                var lmap = L.map(element[0], options)
                    .addLayer(interactiveLayerGroup);

                $scope.$watch('options', function(newVal, oldVal) {
                    if (!newVal) return;
                    lmap.setView(new L.LatLng(newVal.lat, newVal.lng), newVal.zoom);
                }, true);

                $scope.$watch('variables', function(newVal, oldVal) {
                    return $scope.render(newVal.opacity, newVal.year);
                }, true);

                $scope.render = function(opacity, year) {
                    if (!year) {
                        return;
                    }
                    var yearLow = Math.floor(year), newUtfGrid = yearsToUtfGrids[yearLow];
                    for (var iyear=0;iyear<years.length;iyear++) {
                        // XXX: find a better way to set opacity for better ux
                        var x = (year-yearLow);
                        if (years[iyear] == yearLow) {
                            yearsToLayers[yearLow].setOpacity(opacity * (1 - x * x * x));
                        } else if (years[iyear] == yearLow+1) {
                            yearsToLayers[yearLow+1].setOpacity(opacity * (x + x * x)/2);
                        } else {
                            yearsToLayers[years[iyear]].setOpacity(0);
                        }
                    }
                    if (currentUtfGrid) {
                        lmap.removeLayer(currentUtfGrid);
                    }
                    lmap.addLayer(newUtfGrid);
                    currentUtfGrid = newUtfGrid;

                    //Events
                    currentUtfGrid.on('click', function (e) {
                        if (e.data) {
                            onClick(e.data);
                        }
                    });
                    currentUtfGrid.on('mouseover', function (e) {
                        if (e.data) {
                            onMouseOver(e.data);
                        }
                    });
                    currentUtfGrid.on('mouseout', function (e) {
                    });

                }
            }
        }})
    .directive('lineChart', function () {
        return {
            restrict: "A",
            replace: false,
            scope: {
                data: '=',
                name: '='
            },
            link: function($scope, element, attrs, controller) {
                // data = [{name: '', data: ''},]
                var tooltip = d3.select(document.createElement('div'))
                    .append("div")
                    .attr("class", "popover")
                    .style("display", 'none');
                document.body.appendChild(tooltip.node());

                var margin = {top: 20, right: 20, bottom: 30, left: 50},
                    width = 400 - margin.left - margin.right,
                    height = 250 - margin.top - margin.bottom;
                var parseDate = d3.time.format("%Y").parse;
                var svg;

                $scope.render = function(data) {
                    if (!data) {
                        return;
                    }
                    d3.select(element[0]).selectAll('svg').remove();

                    var svg = d3.select(element[0])
                        .append("svg")
                        .attr("width", width + margin.left + margin.right)
                        .attr("height", height + margin.top + margin.bottom)
                        .append("g")
                        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
                    // take the first series to set x
                    var x = d3.time.scale()
                        .range([0, width])
                        .domain(d3.extent(data[0].data, function(d){return parseDate(d[0].toString())}));
                    var y = d3.scale.linear()
                        .range([height, 0])
                        .domain([
                            d3.min(data, function(line) {return d3.min(line.data, function(d){return d[1]})}) * 0.95,
                            d3.max(data, function(line) {return d3.max(line.data, function(d){return d[1]})}) * 1.05
                            ]);
                    //Create SVG element
                    var line = d3.svg.line()
                        .x(function(d){return x(parseDate(d[0].toString()))})
                        .y(function(d){return y(d[1])});
                    var xAxis = d3.svg.axis()
                        .scale(x)
                        .orient('bottom')
                        .ticks(d3.time.years, 5);
                    svg.append("g")
                        .attr('class', 'x axis')
                        .attr("transform", "translate(0," + height + ")")
                        .call(xAxis);
                    var yAxis = d3.svg.axis().scale(y).orient("left");
                    svg.append("g")
                        .attr('class', 'y axis')
                        .call(yAxis)
                        .append("text")
                        .attr("transform", "rotate(-90)")
                        .attr("y", 6)
                        .attr("dy", ".71em")
                        .style("text-anchor", "end")
                        .text($scope.name);
                    var lines = svg.selectAll(".gline")
                        .data(data)
                        .enter().append("g")
                        .attr("class", "gline");
                    lines.append("path")
                        .attr("class", "line")
                        .attr("d", function(d) { return line(d.data); })
                        .style("stroke", function(d) { return d.color; });
                    lines.selectAll("circle")
                        .data(function(d, i) { return d.data.map(function(el){return [el[0], el[1], d.color]});})
                        .enter()
                        .append("circle")
                        .attr("stroke", function(d, i) {return d[2]})
                        .attr("r", 4)
                        .attr("cx", function(d) { return x(parseDate(d[0].toString())); })
                        .attr("cy", function(d) { return y(d[1]); })
                        .attr("class", "linecircle")
                        .on("mouseover", function(d) {
                            tooltip.html("
 " +
                                d[0] + " : " + d3.format(".2f")(d[1]) +
                                "
");
                            tooltip.style("left", d3.event.pageX + 10 + "px");
                            tooltip.style("top", d3.event.pageY - 50 + "px");
                            tooltip.style("display", 'block')
                        })
                        .on("mouseout", function(d) {
                            tooltip.style("display", 'none')
                        })
                        .style("pointer-events","all")
                        .append('title')
                        .text(function(d) { return d[0] + ' : ' + d[1];});
                }
                $scope.$watchCollection('data', function(newData, oldData) {
                    $scope.render(newData);
                });
            }
        }
    });
<commit_msg>Update to new api root url<commit_after>angular.module('app', ['ui.router', 'ui.bootstrap'])
    .constant('API_ROOT_URL', 'http://www.nosfinanceslocales.fr/api')
    .constant('TILES_ROOT_URL', 'http://{s}.tile.nosfinanceslocales.fr/tiles') // get this info from server ?
    .constant('THUMBNAILS_URL', '/static/thumbnails')
    .constant('TEMPLATE_URL', '/static/templates')
    .factory('mapUtils', function(TILES_ROOT_URL, THUMBNAILS_URL) {
        return {
            getTileUrl: function(map_id) {
                return TILES_ROOT_URL + '/' + map_id + "/{z}/{x}/{y}.png";
            },
            getGridUrl: function(map_id) {
                return TILES_ROOT_URL + '/' + map_id + "/{z}/{x}/{y}.grid.json";
            },
            getThumbnailUrl: function(map_id) {
                return THUMBNAILS_URL + '/' + map_id + '.png';
            }
        };
    })
    .factory('Resource', function($http, API_ROOT_URL) {
        function Factory(name) {
            var resource = {
                get: function(id) {
                    return $http.get(API_ROOT_URL + '/' + name + '/' + id)
                        .then(function(response) {
                            return response.data.results;
                        });
                },
                all: function() {
                    return $http.get(API_ROOT_URL + '/' + name + 's')
                        .then(function(response) {
                            return response.data.results;
                        });
                }
            }
            return resource;
        }
        return Factory
    })
    .factory('CitySearch', function($http, API_ROOT_URL) {
        return {
            get: function(term) {
                return $http.get(API_ROOT_URL + '/' + 'city_search' + '?term=' + term)
                    .then(function(response) {
                        return response.data.results;
                    });
            }
        }
    })
    .config(
        [ '$stateProvider', '$urlRouterProvider', 'TEMPLATE_URL',
        function ($stateProvider, $urlRouterProvider, TEMPLATE_URL) {
            $urlRouterProvider.when('', '/maps/');
            $urlRouterProvider.otherwise('/maps/');
            $stateProvider
                .state('about', {
                    url: '/about',
                    templateUrl: TEMPLATE_URL + '/about.html',
                })
                .state('localfinance', {
                    url: '/{id:[0-9]{1,4}}',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/localfinance.detail.html',
                            controller: 'LocalFinanceDetailCtrl'
                        },
                    }
                })
                .state('maps', {
                    abstract: true,
                    url: '/maps',
                    templateUrl: TEMPLATE_URL + '/maps.html',
                    controller: 'MapsCtrl',
                    resolve: {
                        timemaps: function(Resource) {
                            return Resource('timemap').all()
                        },
                        stats: function(Resource) {
                            return Resource('stat').all();
                        }
                    }
                })
                .state('maps.list', {
                    url: '/',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/map.list.html',
                            controller: 'MapListCtrl'
                        }
                    }
                })
                .state('maps.detail', {
                    url: '/{var_name}',
                    views: {
                        '': {
                            templateUrl: TEMPLATE_URL + '/map.detail.html',
                            controller: 'MapDetailCtrl'
                        }
                    }
                });
        }])
    .controller('Home', ['$scope', '$state',
        function($scope, $state, stats) {
            $scope.$state = $state;
            $scope.stats = stats;
    }])
    .controller('MapsCtrl', ['$scope', '$state', 'mapUtils', 'timemaps', 'stats',
        function($scope, $state, mapUtils, timemaps, stats) {
            $scope.mapUtils = mapUtils;
            $scope.timemaps = timemaps;
            $scope.stats = stats;
        }])
    .controller('MapListCtrl', ['$scope', '$state', '$timeout',
        function($scope, $state, $timeout) {
            $scope.thumbailUrls = [];
            function fireDigestEverySecond() {
                $timeout(fireDigestEverySecond , 1000);
            };
            fireDigestEverySecond();
        }])
    .controller('MapDetailCtrl', ['$scope', '$stateParams', 'Resource', 'mapUtils', 'CitySearch',
        function($scope, $stateParams, Resource, mapUtils, CitySearch) {
            $scope.getCities = CitySearch.get;
            $scope.centerMapAndLoadStatCity = function(city) {
                // first update map
                $scope.mapOptions = {
                    zoom: 8,
                    lat: city.lat,
                    lng: city.lng
                }

                // then get stats of the city
                $scope.loadCityFinance(city.id);
            };
            var colors = d3.scale.category10();;
            // get the timemap for this variable
            $scope.timemap = $scope.timemaps.filter(function(timemap) {
                return (timemap.var_name == $stateParams.var_name)
            })[0];
            // same for stats
            $scope.linesData = [
                {
                    name: 'FRANCE',
                    data: $scope.stats.filter(function(stat){
                        return (stat.var_name == $stateParams.var_name)
                    })[0].mean_by_year,
                    color: colors('FRANCE')
                }
            ];
            $scope.removeLine = function(id) {
                // make impossible to remove france
                if (id) {
                    $scope.linesData.some(function(d, i) {
                        if (d.id==id){
                            $scope.linesData.splice(i, i);
                            return true;
                        }
                    });
                }
            }
            // take first map
            $scope.year = $scope.timemap.maps[0].year;
            $scope.opacity = 0.8;
            function findMapData(year){
                return $scope.timemap.maps.filter(function(m) {
                    return (m.year == year)
                })[0];
            }
            $scope.mapData = findMapData($scope.timemap.maps[0].year);
            $scope.loadCityFinance = function(id) {
                function checkLine(id) {
                    return ($scope.linesData.filter(function(d){return (id==d.id)}).length > 0)
                }
                if  (checkLine(id)) {
                    return ;
                }
                Resource('finance').get(id).then(function(results) {
                    if (results.length == 0) {
                        console.log("No data");
                        return;
                    }
                    // get var_name stat
                    var res = results.map(function(d){
                        return [d.year, parseFloat(d.data[$scope.timemap.var_name])];
                    })
                    if (!checkLine(id)) {
                        $scope.linesData.push({name: results[0].name, data: res, color: colors(results[0].name), id: id});
                    }
                });
            }
            $scope.onClick = function(data) {
                $scope.loadCityFinance(data.id);
                $scope.$apply();
            }
            $scope.onMouseOver = function(data) {
                $scope.mouseOverdata = data;
                $scope.$apply();
            }
            document.data = $scope.linesData;
        }])
    .directive('leafletMap', function (mapUtils) {
        return {
            restrict: "A",
            replace: false,
            scope: {
                timemap: '=',
                variables: '=',
                options: '=',
                click: '&onClick',
                mouseOver: '&onMouseOver',
                mouseOut: '&onMouseOut'
            },
            link: function($scope, element, attrs, controller) {
                var onClick = $scope.click(),
                    onMouseOver = $scope.mouseOver(),
                    onMouseOut = $scope.mouseOut();
                var refmap = $scope.timemap.maps[0];

                var southWest = L.latLng(refmap.extent[1],
                                         refmap.extent[0]),
                    northEast = L.latLng(refmap.extent[3],
                                         refmap.extent[2]),
                    bounds = L.latLngBounds(southWest, northEast);
                var stamenAttribution = 'Map tiles by Stamen Design, under CC BY 3.0';
                var basemap = new L.TileLayer(
                        "http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png",
                        {attribution: stamenAttribution});
                var layers = [basemap];
                var yearsToLayers = {}, yearsToUtfGrids = {}, currentUtfGrid, years = [];
                var rcAttribution = 'Data freed by Regards Citoyens '
                for(var imap=0;imap<$scope.timemap.maps.length;imap++) {
                    var map = $scope.timemap.maps[imap];
                    var layer = new L.TileLayer(
                        mapUtils.getTileUrl(map.id),
                        {opacity: 0, subdomains: 'abc',
                         attribution: rcAttribution}
                    );
                    layers.push(layer);
                    yearsToLayers[map.year] = layer;
                    yearsToUtfGrids[map.year] = new L.UtfGrid(mapUtils.getGridUrl(map.id), {useJsonP: false});
                    years.push(map.year);
                }

                var interactiveLayerGroup = L.layerGroup(layers);

                var options = {
                    center: L.latLng(46.22587886848165, 2.21040301636657),
                    zoom: 6,
                    minZoom: refmap.minzoom, // should take the max of min
                    maxZoom: refmap.maxzoom, // should take the min of max
                };

                // leaflet map
                var lmap = L.map(element[0], options)
                    .addLayer(interactiveLayerGroup);

                $scope.$watch('options', function(newVal, oldVal) {
                    if (!newVal) return;
                    lmap.setView(new L.LatLng(newVal.lat, newVal.lng), newVal.zoom);
                }, true);

                $scope.$watch('variables', function(newVal, oldVal) {
                    return $scope.render(newVal.opacity, newVal.year);
                }, true);

                $scope.render = function(opacity, year) {
                    if (!year) {
                        return;
                    }
                    var yearLow = Math.floor(year), newUtfGrid = yearsToUtfGrids[yearLow];
                    for (var iyear=0;iyear<years.length;iyear++) {
                        // XXX: find a better way to set opacity for better ux
                        var x = (year-yearLow);
                        if (years[iyear] == yearLow) {
                            yearsToLayers[yearLow].setOpacity(opacity * (1 - x * x * x));
                        } else if (years[iyear] == yearLow+1) {
                            yearsToLayers[yearLow+1].setOpacity(opacity * (x + x * x)/2);
                        } else {
                            yearsToLayers[years[iyear]].setOpacity(0);
                        }
                    }
                    if (currentUtfGrid) {
                        lmap.removeLayer(currentUtfGrid);
                    }
                    lmap.addLayer(newUtfGrid);
                    currentUtfGrid = newUtfGrid;

                    //Events
                    currentUtfGrid.on('click', function (e) {
                        if (e.data) {
                            onClick(e.data);
                        }
                    });
                    currentUtfGrid.on('mouseover', function (e) {
                        if (e.data) {
                            onMouseOver(e.data);
                        }
                    });
                    currentUtfGrid.on('mouseout', function (e) {
                    });

                }
            }
        }})
    .directive('lineChart', function () {
        return {
            restrict: "A",
            replace: false,
            scope: {
                data: '=',
                name: '='
            },
            link: function($scope, element, attrs, controller) {
                // data = [{name: '', data: ''},]
                var tooltip = d3.select(document.createElement('div'))
                    .append("div")
                    .attr("class", "popover")
                    .style("display", 'none');
                document.body.appendChild(tooltip.node());

                var margin = {top: 20, right: 20, bottom: 30, left: 50},
                    width = 400 - margin.left - margin.right,
                    height = 250 - margin.top - margin.bottom;
                var parseDate = d3.time.format("%Y").parse;
                var svg;

                $scope.render = function(data) {
                    if (!data) {
                        return;
                    }
                    d3.select(element[0]).selectAll('svg').remove();

                    var svg = d3.select(element[0])
                        .append("svg")
                        .attr("width", width + margin.left + margin.right)
                        .attr("height", height + margin.top + margin.bottom)
                        .append("g")
                        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
                    // take the first series to set x
                    var x = d3.time.scale()
                        .range([0, width])
                        .domain(d3.extent(data[0].data, function(d){return parseDate(d[0].toString())}));
                    var y = d3.scale.linear()
                        .range([height, 0])
                        .domain([
                            d3.min(data, function(line) {return d3.min(line.data, function(d){return d[1]})}) * 0.95,
                            d3.max(data, function(line) {return d3.max(line.data, function(d){return d[1]})}) * 1.05
                            ]);
                    //Create SVG element
                    var line = d3.svg.line()
                        .x(function(d){return x(parseDate(d[0].toString()))})
                        .y(function(d){return y(d[1])});
                    var xAxis = d3.svg.axis()
                        .scale(x)
                        .orient('bottom')
                        .ticks(d3.time.years, 5);
                    svg.append("g")
                        .attr('class', 'x axis')
                        .attr("transform", "translate(0," + height + ")")
                        .call(xAxis);
                    var yAxis = d3.svg.axis().scale(y).orient("left");
                    svg.append("g")
                        .attr('class', 'y axis')
                        .call(yAxis)
                        .append("text")
                        .attr("transform", "rotate(-90)")
                        .attr("y", 6)
                        .attr("dy", ".71em")
                        .style("text-anchor", "end")
                        .text($scope.name);
                    var lines = svg.selectAll(".gline")
                        .data(data)
                        .enter().append("g")
                        .attr("class", "gline");
                    lines.append("path")
                        .attr("class", "line")
                        .attr("d", function(d) { return line(d.data); })
                        .style("stroke", function(d) { return d.color; });
                    lines.selectAll("circle")
                        .data(function(d, i) { return d.data.map(function(el){return [el[0], el[1], d.color]});})
                        .enter()
                        .append("circle")
                        .attr("stroke", function(d, i) {return d[2]})
                        .attr("r", 4)
                        .attr("cx", function(d) { return x(parseDate(d[0].toString())); })
                        .attr("cy", function(d) { return y(d[1]); })
                        .attr("class", "linecircle")
                        .on("mouseover", function(d) {
                            tooltip.html("
 " +
                                d[0] + " : " + d3.format(".2f")(d[1]) +
                                "
");
                            tooltip.style("left", d3.event.pageX + 10 + "px");
                            tooltip.style("top", d3.event.pageY - 50 + "px");
                            tooltip.style("display", 'block')
                        })
                        .on("mouseout", function(d) {
                            tooltip.style("display", 'none')
                        })
                        .style("pointer-events","all")
                        .append('title')
                        .text(function(d) { return d[0] + ' : ' + d[1];});
                }
                $scope.$watchCollection('data', function(newData, oldData) {
                    $scope.render(newData);
                });
            }
        }
    });
<|endoftext|>


<commit_before>var events = require('events');
var util = require('util');
var crypto = require('crypto');
var Logger = require('./logger');

/**
 * Queue. Do not instantiate directly, instead use {Bus#queue} to create a new Queue.
 * @param bus
 * @param name
 * @constructor
 */
function Queue(bus, name) {
  events.EventEmitter.call(this);
  this.bus = bus;
  this.id = "bus:queue:" + name;
  this.logger = bus.logger.withTag(this.id);
  this.name = name;
  this.attached = false;
  this.messagesKey = this.id + ":messages";
  this.metadataKey = this.id + ":metadata";
  this.messageAvailableChannel = this.id + ":available";
  this.qKeys = [this.metadataKey, this.messagesKey];
  this.toucher = null;
  this._pushed = 0;
  this._consumed = 0;
}

util.inherits(Queue, events.EventEmitter);

/**
 * Setup association to the connection
 * @private
 */
Queue.prototype._connect = function(cb) {
  if (this.connection) {
    return cb && cb();
  }

  var _this = this;
  var consuming = false;

  // get a connection to use for this queue.
  // it is expected that if the queue already exists
  // in one of the redis servers then a connection to
  // that server will be provided
  this.bus._connection(this.metadataKey, function(connection) {
    if (!connection) {
      return _this.emit('error', 'no connection available');
    }

    function _endOrClose() {
      consuming = _this.isConsuming();
      if (_this.isAttached()) {
        _this.detach();
      }
    }

    _this.connection = connection;
    _this.connection.on('end', _endOrClose);
    _this.connection.on('close', _endOrClose);
    _this.connection.on('drain', function() {
      _this.emit('drain');
    });
    _this.connection.on('ready', function() {
      //_this.logger.debug("connection to queue resumed");
      _this.attach(function() {
        if (consuming) {
          _this.consume();
        }
      });
    });
    cb && cb();
  });
}

/**
 * Emit the attached event. This sets an interval timer to continuously refresh the expiration of the queue keys.
 * @private
 */
Queue.prototype._emitAttached = function(exists) {
  if (this.isAttached()) {
    return;
  }

  var _this = this;
  this.ttl(function(ttl) {
    if (!ttl) {
      return _this.emit('error', 'queue not found');
    }
    _this.attached = true;
    // start the touch timer.
    // we touch the various queue keys to keep them alive for as long as we're attached.
    // the interval time is a third of the ttl time.
    var touchInterval = ttl / 3;
    _this._touch(ttl);
    _this.toucher = setInterval(function() {
      _this._touch(ttl);
    }, (touchInterval * 1000));
    _this.emit('attached', exists);
  });
}

/**
 * set the queue keys expiration to the ttl time (to keep them alive)
 * @param ttl
 * @private
 */
Queue.prototype._touch = function(ttl) {
  var _this = this;
  this.qKeys.forEach(function(key) {
    _this.connection.expire(key, ttl, function(err, resp) {
      if (err) {
        return _this.emit('error', "error setting key " + key + " expiration to " + ttl + ": " + err);
      }
    });
  })
}

/**
 * Emit the detached event. Clears the interval timer that refreshes the queue keys expiration
 * @private
 */
Queue.prototype._emitDetached = function() {
  if (!this.isAttached()) {
    return;
  }
  clearInterval(this.toucher);
  this.toucher = null;
  this.attached = false;
  this.emit('detached');
}

/**
 * Return true if we are attached to the queue
 */
Queue.prototype.isAttached = function() {
  return this.attached;
}

/**
 * Attach to a queue, optionally setting some options.
 * Events:
 *  - attaching - starting to attach to the queue
 *  - attached - messages can be pushed to the queue or consumed from it.
 *               receives a boolean indicating whether the queue already existed (true) or not (false)
 *  - error - some error occurred
 */
Queue.prototype.attach = function(options) {
  if (this.isAttached()) {
    return this.emit('error', 'cannot attach to queue: already attached');
  }

  var _this = this;
  function _attach() {
    _this.emit('attaching');
    options = options || {};
    options.ttl = options.ttl || 30; // default ttl of 30 seconds

    _this.exists(function(exists) {
      if (!exists) {
        // set options for the queue, creating the metadata key
        return _this.option('ttl', options.ttl, function() {
          _this._emitAttached(exists);
        });
      }
      // queue already exists, do not set options
      _this._emitAttached(exists);
    });
  };

  this._connect(_attach);
}

/**
 * Detach from the queue.
 * No further messages can be pushed or consumed until attached again.
 * This does not remove the queue. Other clients may still push and consume messages from the queue.
 * If this is the last client that detaches, then the queue will automatically be destroyed if no
 * client attaches to it within the defined ttl of the queue.
 * Events:
 *  - detached - detached from the queue
 *  - error - some error occurred
 */
Queue.prototype.detach = function() {
  if (!this.isAttached()) {
    return this.emit('error', 'cannot detach from queue: already detached');
  }

  this.emit('detaching');
  this.stop();
  this._emitDetached();
};

/**
 * Get the ttl of the queue. The ttl is the time in seconds for the queue to live without any clients attached to it.
 * @param cb receives the ttl
 */
Queue.prototype.ttl = function(cb) {
  this.option('ttl', function(ttl) {
    if (ttl !== null) {
      ttl = parseInt(ttl);
    }
    cb && cb(ttl);
  });
};

/**
 * Get or set an option by name
 * @param name the option name. If a value is not provided, the option will be retrieved.
 * @param value if a value is provided, the option will be set to the value
 * @param cb if getting, will be provided with the value, if setting will be called upon success.
 */
Queue.prototype.option = function(name, value, cb) {
  var _this = this;
  if (typeof value === 'function') {
    cb = value;
    value = null;
  }

  if (value) {
    this.connection.hsetnx(this.metadataKey, name, value, function(err) {
      if (err) {
        return _this.emit('error', "error creating queue metadata: " + err);
      }
      cb && cb();
    });
  } else {
    this.connection.hget(this.metadataKey, name, function(err, resp) {
      if (err) {
        return _this.emit('error', "error reading metadata option " + name + ": " + err);
      }
      cb && cb(resp);
    });
  }
};

/**
 * Closes the queue and destroys all pending messages. No more messages can be pushed or consumed.
 * Clients attempting to attach to the queue will receive the closed event.
 * Clients currently attached to the queue will receive the closed event.
 * Events:
 *  - error - some error occurred
 *  - closed - the queue was closed
 */
Queue.prototype.close = function() {
  if (!this.isAttached()) {
    var err = 'not attached';
    return this.emit('error', "error closing queue: " + err);
  }

  var _this = this;
  this.detach();

  var closes = 0;

  // delete the metadata key
  this.qKeys.forEach(function(key) {
    ++closes;
    _this._deleteKey(key, function() {
      if (--closes === 0) {
        _this.emit('closed');
      }
    });
  });
};

/**
 * Delete a key from redis
 * @private
 */
Queue.prototype._deleteKey = function(key, cb) {
  var _this = this;
  this.connection.del(key, function(err, resp) {
    if (err) {
      _this.emit('error', "error deleting key: " + err);
    }
    cb && cb(resp);
  });
}

/**
 * Check if the queue exists.
 * @param cb receives true if the queue exists and false if not
 */
Queue.prototype.exists = function(cb) {
  var _this = this;
  function _exists() {
    // check if the metadata exists, as the queue itself might not contain any messages
    // meaning that it doesn't actually exist
    _this.connection.exists(_this.metadataKey, function(err, exists) {
      if (err) {
        return _this.emit('error', "error checking if queue exists: " + err);
      }
      cb && cb(exists === 1);
    });
  }
  this._connect(_exists);

};

/**
 * Push a message to the queue.
 * The message will remain in the queue until a consumer reads it
 * or until the queue is closed or until it expires.
 * @param message string or object
 * @param cb invoked when the push was actually performed
 * @return returns true if the commands are successfully flushed to the kernel for immediate sending,
 *         and false if the buffer is full and the commands are queued to be sent when the buffer is ready again
 */
Queue.prototype.push = function(message, cb) {
  if (!this.isAttached()) {
    var err = 'not attached';
    return this.emit('error', "error pushing to queue: " + err);
  }

  if (typeof message === 'object') {
    message = JSON.stringify(message);
  }

  // push the message to the queue
  var _this = this;
  var pushed = ++_this._pushed;
  var multi = this.connection.multi();
  multi.rpush(this.messagesKey, message, function(err, resp){
    if (err) {
      return _this.emit('error', "error pushing to queue (rpush): " + err);
    }
  });
  multi.publish(this.messageAvailableChannel, pushed, function(err, resp){
    if (err) {
      return _this.emit('error', "error pushing to queue (publish): " + err);
    }
//    console.log('[%s] pushed %s', _this.name, pushed);
    cb && cb();
  });
  return multi.exec(function(err) {
    if (err) {
      return _this.emit('error', "error pushing to queue (exec): " + err);
    }
  });

};

/**
 * Returns the number of messages pushed to this queue
 * @returns {number}
 */
Queue.prototype.pushed = function() {
  return this._pushed;
}

/**
 * Returns the number of messages consumed by this queue
 * @returns {number}
 */
Queue.prototype.consumed = function() {
  return this._consumed;
}

/**
 * Set the consuming state and emit it
 * @param state
 * @private
 */
Queue.prototype._consuming = function(state) {
  this.consuming = state;
  this.emit('consuming', state);
}

/**
 * Stop consuming messages. This will prevent further reading of messages from the queue.
 * Events:
 *  - consuming - the new consuming state, which will be false when no longer consuming
 *  - error - on some error
 */
Queue.prototype.stop = function() {
  if (!this.isConsuming()) {
    return;
  }

  this.bus._unsubscribe(this.connection, this.messageAvailableChannel);
  this._consuming(false);
}

/**
 * Returns true of this queue is consuming messages
 */
Queue.prototype.isConsuming = function() {
  return this.consuming;
}

/**
 * Read a single message from the queue.
 * Will continue to read messages until there are no more messages to read.
 * @private
 */
Queue.prototype._consumeMessages = function() {
  if (!this.isConsuming()) {
    return;
  }

  this._popping = true;
  var _this = this;
  function _pop() {
    _this.connection.lpop(_this.messagesKey, function(err, message) {
      if (err) {
        return _this.emit('error', "error consuming message: " + err);
      }

      if (message) {
        ++_this._consumed;
        //      console.log('[%s] consumed %s', _this.name, _this._consumed);
        // emit the message to the consumer
        _this.emit('message', message);
        // go fetch another one
        _pop();
      } else if (_this._messageAvailable) {
        // we received a push event to the queue while we were popping.
        // to make sure the event wasn't received between the time that
        // redis return a null message
        _this._messageAvailable = false;
        _pop();
      } else {
        _this._popping = false;
      }
    });
  }
  _pop();
}

/**
 * handle the event that a message was inserted into the queue
 * @param channel
 * @param event
 * @private
 */
Queue.prototype._handleQueueEvent = function(channel, message) {
  if (channel === this.messageAvailableChannel) {
    if (this._popping) {
      this._messageAvailable = true;
    } else {
      this._messageAvailable = false;
      this._consumeMessages();
    }
  }
}

/**
 * Consume messages form the queue. Must be attached to the queue or an error will be emitted.
 * To stop consuming messages call Queue#stop.
 * Events:
 *  - consuming - the new consuming state, which will be true, after which message events will start being fired
 *  - message - received a message from the queue
 *  - error - not attached, the queue does not exist, or some error occurred
 */
Queue.prototype.consume = function() {
  if (this.isConsuming()) {
    return;
  }

  if (!this.isAttached()) {
    var err = 'not attached';
    return this.emit('error', "error consuming from queue: " + err);
  }

  var _this = this;
  function _event(type, channel, message) {
    switch (type) {
      case 'subscribe':
        _this._consuming(true);
        // also immediately try to consume messages from the queue
        _this._consumeMessages();
        break;
      case 'unsubscribe':
        break;
      case 'event':
        _this._handleQueueEvent(channel, message);
        break;
    }
  }

  this.bus._subscribe(this.connection, this.messageAvailableChannel, _event);

};

exports = module.exports = Queue;
<commit_msg>allow pushing message before attaching to queue, store then internally until attached<commit_after>var events = require('events');
var util = require('util');
var crypto = require('crypto');
var Logger = require('./logger');

/**
 * Queue. Do not instantiate directly, instead use {Bus#queue} to create a new Queue.
 * @param bus
 * @param name
 * @constructor
 */
function Queue(bus, name) {
  events.EventEmitter.call(this);
  this.bus = bus;
  this.id = "bus:queue:" + name;
  this.logger = bus.logger.withTag(this.id);
  this.name = name;
  this.attached = false;
  this.messagesKey = this.id + ":messages";
  this.metadataKey = this.id + ":metadata";
  this.messageAvailableChannel = this.id + ":available";
  this.qKeys = [this.metadataKey, this.messagesKey];
  this.toucher = null;
  this._pushed = 0;
  this._consumed = 0;
  this._offlineQueue = [];
}

util.inherits(Queue, events.EventEmitter);

/**
 * Setup association to the connection
 * @private
 */
Queue.prototype._connect = function(cb) {
  if (this.connection) {
    return cb && cb();
  }

  var _this = this;
  var consuming = false;

  // get a connection to use for this queue.
  // it is expected that if the queue already exists
  // in one of the redis servers then a connection to
  // that server will be provided
  this.bus._connection(this.metadataKey, function(connection) {
    if (!connection) {
      return _this.emit('error', 'no connection available');
    }

    function _endOrClose() {
      consuming = _this.isConsuming();
      if (_this.isAttached()) {
        _this.detach();
      }
    }

    _this.connection = connection;
    _this.connection.on('end', _endOrClose);
    _this.connection.on('close', _endOrClose);
    _this.connection.on('drain', function() {
      _this.emit('drain');
    });
    _this.connection.on('ready', function() {
      //_this.logger.debug("connection to queue resumed");
      _this.attach(function() {
        if (consuming) {
          _this.consume();
        }
      });
    });
    cb && cb();
  });
}

/**
 * Emit the attached event. This sets an interval timer to continuously refresh the expiration of the queue keys.
 * @private
 */
Queue.prototype._emitAttached = function(exists) {
  if (this.isAttached()) {
    return;
  }

  var _this = this;
  this.ttl(function(ttl) {
    if (!ttl) {
      return _this.emit('error', 'queue not found');
    }
    _this.attached = true;
    // start the touch timer.
    // we touch the various queue keys to keep them alive for as long as we're attached.
    // the interval time is a third of the ttl time.
    var touchInterval = ttl / 3;
    _this._touch(ttl);
    _this.toucher = setInterval(function() {
      _this._touch(ttl);
    }, (touchInterval * 1000));

    // we're attached, send all messages that were pushed when we were not yet attached
    _this._offlineQueue.forEach(function(message) {
      _this.push(message);
    });
    _this._offlineQueue = [];
    _this.emit('attached', exists);
  });
}

/**
 * set the queue keys expiration to the ttl time (to keep them alive)
 * @param ttl
 * @private
 */
Queue.prototype._touch = function(ttl) {
  var _this = this;
  this.qKeys.forEach(function(key) {
    _this.connection.expire(key, ttl, function(err, resp) {
      if (err) {
        return _this.emit('error', "error setting key " + key + " expiration to " + ttl + ": " + err);
      }
    });
  })
}

/**
 * Emit the detached event. Clears the interval timer that refreshes the queue keys expiration
 * @private
 */
Queue.prototype._emitDetached = function() {
  if (!this.isAttached()) {
    return;
  }
  clearInterval(this.toucher);
  this.toucher = null;
  this.attached = false;
  this.emit('detached');
}

/**
 * Return true if we are attached to the queue
 */
Queue.prototype.isAttached = function() {
  return this.attached;
}

/**
 * Attach to a queue, optionally setting some options.
 * Events:
 *  - attaching - starting to attach to the queue
 *  - attached - messages can be pushed to the queue or consumed from it.
 *               receives a boolean indicating whether the queue already existed (true) or not (false)
 *  - error - some error occurred
 */
Queue.prototype.attach = function(options) {
  if (this.isAttached()) {
    return this.emit('error', 'cannot attach to queue: already attached');
  }

  var _this = this;
  function _attach() {
    _this.emit('attaching');
    options = options || {};
    options.ttl = options.ttl || 30; // default ttl of 30 seconds

    _this.exists(function(exists) {
      if (!exists) {
        // set options for the queue, creating the metadata key
        return _this.option('ttl', options.ttl, function() {
          _this._emitAttached(exists);
        });
      }
      // queue already exists, do not set options
      _this._emitAttached(exists);
    });
  };

  this._connect(_attach);
}

/**
 * Detach from the queue.
 * No further messages can be pushed or consumed until attached again.
 * This does not remove the queue. Other clients may still push and consume messages from the queue.
 * If this is the last client that detaches, then the queue will automatically be destroyed if no
 * client attaches to it within the defined ttl of the queue.
 * Events:
 *  - detached - detached from the queue
 *  - error - some error occurred
 */
Queue.prototype.detach = function() {
  if (!this.isAttached()) {
    return this.emit('error', 'cannot detach from queue: already detached');
  }

  this.emit('detaching');
  this.stop();
  this._emitDetached();
};

/**
 * Get the ttl of the queue. The ttl is the time in seconds for the queue to live without any clients attached to it.
 * @param cb receives the ttl
 */
Queue.prototype.ttl = function(cb) {
  this.option('ttl', function(ttl) {
    if (ttl !== null) {
      ttl = parseInt(ttl);
    }
    cb && cb(ttl);
  });
};

/**
 * Get or set an option by name
 * @param name the option name. If a value is not provided, the option will be retrieved.
 * @param value if a value is provided, the option will be set to the value
 * @param cb if getting, will be provided with the value, if setting will be called upon success.
 */
Queue.prototype.option = function(name, value, cb) {
  var _this = this;
  if (typeof value === 'function') {
    cb = value;
    value = null;
  }

  if (value) {
    this.connection.hsetnx(this.metadataKey, name, value, function(err) {
      if (err) {
        return _this.emit('error', "error creating queue metadata: " + err);
      }
      cb && cb();
    });
  } else {
    this.connection.hget(this.metadataKey, name, function(err, resp) {
      if (err) {
        return _this.emit('error', "error reading metadata option " + name + ": " + err);
      }
      cb && cb(resp);
    });
  }
};

/**
 * Closes the queue and destroys all pending messages. No more messages can be pushed or consumed.
 * Clients attempting to attach to the queue will receive the closed event.
 * Clients currently attached to the queue will receive the closed event.
 * Events:
 *  - error - some error occurred
 *  - closed - the queue was closed
 */
Queue.prototype.close = function() {
  if (!this.isAttached()) {
    var err = 'not attached';
    return this.emit('error', "error closing queue: " + err);
  }

  var _this = this;
  this.detach();

  var closes = 0;

  // delete the metadata key
  this.qKeys.forEach(function(key) {
    ++closes;
    _this._deleteKey(key, function() {
      if (--closes === 0) {
        _this.emit('closed');
      }
    });
  });
};

/**
 * Delete a key from redis
 * @private
 */
Queue.prototype._deleteKey = function(key, cb) {
  var _this = this;
  this.connection.del(key, function(err, resp) {
    if (err) {
      _this.emit('error', "error deleting key: " + err);
    }
    cb && cb(resp);
  });
}

/**
 * Check if the queue exists.
 * @param cb receives true if the queue exists and false if not
 */
Queue.prototype.exists = function(cb) {
  var _this = this;
  function _exists() {
    // check if the metadata exists, as the queue itself might not contain any messages
    // meaning that it doesn't actually exist
    _this.connection.exists(_this.metadataKey, function(err, exists) {
      if (err) {
        return _this.emit('error', "error checking if queue exists: " + err);
      }
      cb && cb(exists === 1);
    });
  }
  this._connect(_exists);

};

/**
 * Push a message to the queue.
 * The message will remain in the queue until a consumer reads it
 * or until the queue is closed or until it expires.
 * @param message string or object
 * @param cb invoked when the push was actually performed
 * @return {boolean} returns true if the commands are successfully flushed to the kernel for immediate sending,
 *         and false if the buffer is full and the commands are queued to be sent when the buffer is ready again
 */
Queue.prototype.push = function(message, cb) {
  if (!this.isAttached()) {
    // we're not attached yet, store the messages internally and push them all once we're attached
    this._offlineQueue.push(message);
    return false;
  }

  if (typeof message === 'object') {
    message = JSON.stringify(message);
  }

  // push the message to the queue
  var _this = this;
  var pushed = ++_this._pushed;
  var multi = this.connection.multi();
  multi.rpush(this.messagesKey, message, function(err, resp){
    if (err) {
      return _this.emit('error', "error pushing to queue (rpush): " + err);
    }
  });
  multi.publish(this.messageAvailableChannel, pushed, function(err, resp){
    if (err) {
      return _this.emit('error', "error pushing to queue (publish): " + err);
    }
    cb && cb();
  });
  return multi.exec(function(err) {
    if (err) {
      return _this.emit('error', "error pushing to queue (exec): " + err);
    }
  });

};

/**
 * Returns the number of messages pushed to this queue
 * @returns {number}
 */
Queue.prototype.pushed = function() {
  return this._pushed;
}

/**
 * Returns the number of messages consumed by this queue
 * @returns {number}
 */
Queue.prototype.consumed = function() {
  return this._consumed;
}

/**
 * Set the consuming state and emit it
 * @param state
 * @private
 */
Queue.prototype._consuming = function(state) {
  this.consuming = state;
  this.emit('consuming', state);
}

/**
 * Stop consuming messages. This will prevent further reading of messages from the queue.
 * Events:
 *  - consuming - the new consuming state, which will be false when no longer consuming
 *  - error - on some error
 */
Queue.prototype.stop = function() {
  if (!this.isConsuming()) {
    return;
  }

  this.bus._unsubscribe(this.connection, this.messageAvailableChannel);
  this._consuming(false);
}

/**
 * Returns true of this queue is consuming messages
 */
Queue.prototype.isConsuming = function() {
  return this.consuming;
}

/**
 * Read a single message from the queue.
 * Will continue to read messages until there are no more messages to read.
 * @private
 */
Queue.prototype._consumeMessages = function() {
  if (!this.isConsuming()) {
    return;
  }

  this._popping = true;
  var _this = this;
  function _pop() {
    _this.connection.lpop(_this.messagesKey, function(err, message) {
      if (err) {
        return _this.emit('error', "error consuming message: " + err);
      }

      if (message) {
        ++_this._consumed;
        //      console.log('[%s] consumed %s', _this.name, _this._consumed);
        // emit the message to the consumer
        _this.emit('message', message);
        // go fetch another one
        _pop();
      } else if (_this._messageAvailable) {
        // we received a push event to the queue while we were popping.
        // to make sure the event wasn't received between the time that
        // redis return a null message
        _this._messageAvailable = false;
        _pop();
      } else {
        _this._popping = false;
      }
    });
  }
  _pop();
}

/**
 * handle the event that a message was inserted into the queue
 * @param channel
 * @param event
 * @private
 */
Queue.prototype._handleQueueEvent = function(channel, message) {
  if (channel === this.messageAvailableChannel) {
    if (this._popping) {
      this._messageAvailable = true;
    } else {
      this._messageAvailable = false;
      this._consumeMessages();
    }
  }
}

/**
 * Consume messages form the queue. Must be attached to the queue or an error will be emitted.
 * To stop consuming messages call Queue#stop.
 * Events:
 *  - consuming - the new consuming state, which will be true, after which message events will start being fired
 *  - message - received a message from the queue
 *  - error - not attached, the queue does not exist, or some error occurred
 */
Queue.prototype.consume = function() {
  if (this.isConsuming()) {
    return;
  }

  if (!this.isAttached()) {
    var err = 'not attached';
    return this.emit('error', "error consuming from queue: " + err);
  }

  var _this = this;
  function _event(type, channel, message) {
    switch (type) {
      case 'subscribe':
        _this._consuming(true);
        // also immediately try to consume messages from the queue
        _this._consumeMessages();
        break;
      case 'unsubscribe':
        break;
      case 'event':
        _this._handleQueueEvent(channel, message);
        break;
    }
  }

  this.bus._subscribe(this.connection, this.messageAvailableChannel, _event);

};

exports = module.exports = Queue;
<|endoftext|>


<commit_before>// relme.js
//
// get the rel-me links from an HTML page
//
// Copyright 2012, StatusNet Inc. and others
//
// 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.

var async = require("async"),
    HTML5 = require("html5"),
    http = require("http"),
    urlParse = require("url").parse;

var RelMe = {
    TypeError: function(type) {
        this.name    = "RelMe.TypeError";
        this.type    = type;
        this.message = "Bad type: " + type;
    },
    HTTPError: function(code) {
        this.name    = "RelMe.HTTPError";
        this.code    = code;
        this.message = "Bad HTTP status code: " + code;
    },
    getLinks: function(url, callback) {
        var parts = urlParse(url);
        async.waterfall([
            function(cb) {
                var port = parseInt(parts.port, 10) || (parts.protocol === 'https:') ? 443: 80,
                    options = {
                        hostname: parts.hostname,
                        path: parts.path,
                        method: 'GET',
                        headers: {
                            'connection': 'close',
                            'user-agent': 'OpenFollow/0.1.0 (http://openfollow.net/)'
                        }
                    };

                // XXX: use a cache, you big jerk!
                http.get(options, function(res) {
                    cb(null, res);
                }).on('error', function(err) {
                    cb(err, null);
                });
            },
            function(res, cb) {

                var tz, links = [];

                if (res.statusCode !== 200) {
                    cb(new RelMe.HTTPError("Bad response code: " + res.statusCode), null);
                    return;
                }

                if (!res.headers['content-type'] ||
                    res.headers['content-type'].substr(0, 9) !== 'text/html') {
                    cb(new RelMe.TypeError(res.headers['content-type']), null);
                    return;
                }

                tz = new HTML5.Tokenizer(res);

                tz.on("token", function(tok) {
                    var href = null, isRelMe = false, i, attr;
                    switch (tok.type) {
                        case 'StartTag':
                        if (tok.name === 'a') {
                            for (i = 0; i < tok.data.length; i++) {
                                attr = tok.data[i];
                                if (attr.nodeName === 'href') {
                                    href = attr.nodeValue;
                                } else if (attr.nodeName === 'rel' &&
                                           attr.nodeValue === 'me') {
                                    isRelMe = true;
                                }
                            }
                            if (isRelMe && href) {
                                links.push(href);
                            }
                        }
                        break;
                        case 'EOF':
                        cb(null, links);
                        return;
                    }
                });

                tz.tokenize();
            }
        ], callback);
    }
};

// Some housekeeping for our custom classes

RelMe.TypeError.prototype = new Error;
RelMe.TypeError.prototype.constructor = RelMe.TypeError;

RelMe.HTTPError.prototype = new Error;
RelMe.HTTPError.prototype.constructor = RelMe.HTTPError;

module.exports = RelMe;
<commit_msg>ProtocolError<commit_after>// relme.js
//
// get the rel-me links from an HTML page
//
// Copyright 2012, StatusNet Inc. and others
//
// 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.

var async = require("async"),
    HTML5 = require("html5"),
    http = require("http"),
    https = require("https"),
    urlParse = require("url").parse;

var RelMe = {
    TypeError: function(type) {
        this.name    = "RelMe.TypeError";
        this.type    = type;
        this.message = "Bad type: " + type;
    },
    HTTPError: function(code) {
        this.name    = "RelMe.HTTPError";
        this.code    = code;
        this.message = "Bad HTTP status code: " + code;
    },
    ProtocolError: function(protocol) {
        this.name     = "RelMe.ProtocolError";
        this.protocol = protocol;
        this.message  = "Unrecognized protocol: " + protocol;
    },
    getLinks: function(url, callback) {
        async.waterfall([
            function(cb) {
                var parts = urlParse(url),
                    options = {
                        hostname: parts.hostname,
                        port: parseInt(parts.port, 10) || ((parts.protocol == 'https:') ? 443: 80),
                        path: parts.path,
                        method: 'GET',
                        headers: {
                            'User-Agent': 'OpenFollow/0.1.0 (http://openfollow.net/)'
                        }
                    },
                    mod = (parts.protocol == 'http:') ? http :
                        (parts.protocol == 'https:') ? https :
                        null;

                if (!mod) {
                    cb(new RelMe.ProtocolError(parts.protocol), null);
                    return;
                }

                // XXX: use a cache, you big jerk!
                mod.get(options, function(res) {
                    cb(null, res);
                }).on('error', function(err) {
                    cb(err, null);
                });
            },
            function(res, cb) {

                var tz, links = [];

                if (res.statusCode !== 200) {
                    cb(new RelMe.HTTPError("Bad response code: " + res.statusCode), null);
                    return;
                }

                if (!res.headers['content-type'] ||
                    res.headers['content-type'].substr(0, 9) !== 'text/html') {
                    cb(new RelMe.TypeError(res.headers['content-type']), null);
                    return;
                }

                tz = new HTML5.Tokenizer(res);

                tz.on("token", function(tok) {
                    var href = null, isRelMe = false, i, attr;
                    switch (tok.type) {
                        case 'StartTag':
                        if (tok.name === 'a') {
                            for (i = 0; i < tok.data.length; i++) {
                                attr = tok.data[i];
                                if (attr.nodeName === 'href') {
                                    href = attr.nodeValue;
                                } else if (attr.nodeName === 'rel' &&
                                           attr.nodeValue === 'me') {
                                    isRelMe = true;
                                }
                            }
                            if (isRelMe && href) {
                                links.push(href);
                            }
                        }
                        break;
                        case 'EOF':
                        cb(null, links);
                        return;
                    }
                });

                tz.tokenize();
            }
        ], callback);
    }
};

// Some housekeeping for our custom classes

RelMe.TypeError.prototype = new Error;
RelMe.TypeError.prototype.constructor = RelMe.TypeError;

RelMe.HTTPError.prototype = new Error;
RelMe.HTTPError.prototype.constructor = RelMe.HTTPError;

RelMe.ProtocolError.prototype = new Error;
RelMe.ProtocolError.prototype.constructor = RelMe.ProtocolError;

module.exports = RelMe;
<|endoftext|>


<commit_before>const bunyan = require('bunyan');
const bunyanFormat = require('bunyan-format');
const GitHubApi = require('github');
const Bottleneck = require('bottleneck');
const Context = require('./context');

const logger = bunyan.createLogger({
  name: 'PRobot',
  level: process.env.LOG_LEVEL || 'debug',
  stream: bunyanFormat({outputMode: process.env.LOG_FORMAT || 'short'})
});

class Robot {
  constructor(integration, webhook, cache) {
    this.integration = integration;
    this.webhook = webhook;
    this.cache = cache;
  }

  on(event, callback) {
    const [name, action] = event.split('.');

    return this.webhook.on(name, event => {
      if (!action || action === event.payload.action) {
        callback(event, new Context(event));
      }
    });
  }

  async auth(id) {
    let github;

    if (id) {
      const token = await this.cache.wrap(`integration:${id}:token`, () => {
        this.log.trace(`creating token for installation ${id}`);
        return this.integration.createToken(id);
      }, {ttl: 60 * 60});

      github = new GitHubApi({debug: process.env.LOG_LEVEL === 'trace'});
      github.authenticate({type: 'token', token: token.token});
    } else {
      github = await this.integration.asIntegration();
    }

    return probotEnhancedClient(github);
  }

  log(...args) {
    return logger.debug(...args);
  }
}

function probotEnhancedClient(github) {
  github = rateLimitedClient(github);

  github.paginate = require('./paginate');

  return github;
}

// Hack client to only allow one request at a time with a 1s delay
// https://github.com/mikedeboer/node-github/issues/526
function rateLimitedClient(github) {
  const limiter = new Bottleneck(1, 1000);
  const oldHandler = github.handler;
  github.handler = (msg, block, callback) => {
    limiter.submit(oldHandler.bind(github), msg, block, callback);
  };
  return github;
}

// Add level methods on the logger
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
  Robot.prototype.log[level] = logger[level].bind(logger);
});

module.exports = (...args) => new Robot(...args);
<commit_msg>Show trace for webhooks received<commit_after>const bunyan = require('bunyan');
const bunyanFormat = require('bunyan-format');
const GitHubApi = require('github');
const Bottleneck = require('bottleneck');
const Context = require('./context');

const logger = bunyan.createLogger({
  name: 'PRobot',
  level: process.env.LOG_LEVEL || 'debug',
  stream: bunyanFormat({outputMode: process.env.LOG_FORMAT || 'short'})
});

class Robot {
  constructor(integration, webhook, cache) {
    this.integration = integration;
    this.webhook = webhook;
    this.cache = cache;

    this.webhook.on('*', event => this.log.trace(event, 'webhook received'));
  }

  on(event, callback) {
    const [name, action] = event.split('.');

    return this.webhook.on(name, event => {
      if (!action || action === event.payload.action) {
        callback(event, new Context(event));
      }
    });
  }

  async auth(id) {
    let github;

    if (id) {
      const token = await this.cache.wrap(`integration:${id}:token`, () => {
        this.log.trace(`creating token for installation ${id}`);
        return this.integration.createToken(id);
      }, {ttl: 60 * 60});

      github = new GitHubApi({debug: process.env.LOG_LEVEL === 'trace'});
      github.authenticate({type: 'token', token: token.token});
    } else {
      github = await this.integration.asIntegration();
    }

    return probotEnhancedClient(github);
  }

  log(...args) {
    return logger.debug(...args);
  }
}

function probotEnhancedClient(github) {
  github = rateLimitedClient(github);

  github.paginate = require('./paginate');

  return github;
}

// Hack client to only allow one request at a time with a 1s delay
// https://github.com/mikedeboer/node-github/issues/526
function rateLimitedClient(github) {
  const limiter = new Bottleneck(1, 1000);
  const oldHandler = github.handler;
  github.handler = (msg, block, callback) => {
    limiter.submit(oldHandler.bind(github), msg, block, callback);
  };
  return github;
}

// Add level methods on the logger
['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach(level => {
  Robot.prototype.log[level] = logger[level].bind(logger);
});

module.exports = (...args) => new Robot(...args);
<|endoftext|>


<commit_before>var assert = require('assert');
var Buffer = require('buffer').Buffer;
var fs = require('fs');
var blend = require('..');

if (process.setMaxListeners) process.setMaxListeners(0);

exports['test blending bogus JPEG image'] = function(beforeExit) {
    var completed = false;
    var buffer = new Buffer(32);
    buffer[0] = 255;
    buffer[1] = 216;
    blend([
        buffer,
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        assert.ok(err);
        assert.equal(err.message, "Premature end of JPEG file");
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a malformed jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1d.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        assert.ok(err);
        assert.equal(err.message, "Corrupt JPEG data: bad Huffman code");
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1a.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a grayscale jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1b.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a color jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1c.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
    });

    beforeExit(function() { assert.ok(completed); });
};

<commit_msg>check output with md5<commit_after>var assert = require('assert');
var Buffer = require('buffer').Buffer;
var crypto = require('crypto');
var fs = require('fs');
var blend = require('..');

function md5(buffer) {
    return crypto.createHash('md5').update(buffer).digest('hex');
}

if (process.setMaxListeners) process.setMaxListeners(0);

exports['test blending bogus JPEG image'] = function(beforeExit) {
    var completed = false;
    var buffer = new Buffer(32);
    buffer[0] = 255;
    buffer[1] = 216;
    blend([
        buffer,
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        assert.ok(err);
        assert.equal(err.message, "Premature end of JPEG file");
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a malformed jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1d.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        assert.ok(err);
        assert.equal(err.message, "Corrupt JPEG data: bad Huffman code");
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1a.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
        assert.equal('4e43abdd243ccafc80a7da9b0efe3086', md5(data));
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a grayscale jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1b.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
        assert.equal('6a11a65c397f9f3929791703dd61645f', md5(data));
    });

    beforeExit(function() { assert.ok(completed); });
};

exports['test blending a color jpeg image'] = function(beforeExit) {
    var completed = false;

    blend([
        fs.readFileSync('test/fixture/1c.jpg'),
        fs.readFileSync('test/fixture/2.png')
    ], function(err, data) {
        completed = true;
        if (err) throw err;
        assert.equal('393a40fd34bfd58f4d5c5e658f6f00f1', md5(data));
    });

    beforeExit(function() { assert.ok(completed); });
};

<|endoftext|>


<commit_before>const lab = require("lab").script();
exports.lab = lab;

let Execute = require("../src/index");

lab.experiment("Caching Execution Tree Test", () => {

    lab.test("the result should be {a:1} because of caching", () => {
        let execute = new Execute();

        const func = (data)=> {
            return new Promise((resolve) => {

                setTimeout(()=> {
                    console.log(data);
                    resolve(data);
                }, 0);
            });
        };

        const cacheOptions = {
            enable: true,
            ttl: 60,
            key: (data) => data.sub_id
        };

        let executionTree = Execute.prepareExecutionTree({
            concurrency: 1,
            cache: cacheOptions,
            steps :[
                {
                    title:"step 1",
                    action: (data) => func({a: 1})
                }
            ]
        });

        let executionData = {
            sub_id :123
        };

        return execute.run(executionTree, executionData).then( ()=> {

            executionTree = Execute.prepareExecutionTree({
                concurrency: 1,
                cache: cacheOptions,
                steps :[
                    {
                        title:"step 1",
                        action: (data) => func({a: 2})
                    }
                ]
            });

            return execute.run(executionTree, executionData).then( (result)=> {
                console.log("****result");
                console.log(result);

                lab.expect(result.a).to.equal(1);
            });

        });
    });

    lab.test("statistics should show hits and misses", () => {
        let execute = new Execute();

        const func = (data)=> {
            return new Promise((resolve) => {

                setTimeout(()=> {
                    console.log(data);
                    resolve(data);
                }, 0);
            });
        };

        let newKey = (Math.random()*10000000).toString();

        const cacheOptions = {
            enable: true,
            ttl: 60,
            key: (data) => newKey + data.sub_id
        };

        let executionTree = Execute.prepareExecutionTree({
            concurrency: 1,
            steps :[
                {
                    cache: cacheOptions,
                    title:"step 1",
                    action: (data) => func({a: 1})
                },
                {
                    cache: cacheOptions,
                    title:"step 2",
                    action: (data) => func({b: 2})
                },
                {
                    cache: cacheOptions,
                    title:"step 3",
                    action: (data) => func({c: 3})
                },
                {
                    cache: cacheOptions,
                    title:"step 4",
                    action: (data) => func({d: 4})
                },
                {
                    cache: cacheOptions,
                    title:"step 5",
                    action: (data) => func({e: 5})
                }
            ]
        });

        let executionData = {
            sub_id :123
        };

        return execute.run(executionTree, executionData).then( (result)=> {
            lab.expect(result.a).to.equal(1);

            let stat = Execute.extractStatistics(executionTree);
            let cacheCount =
                stat.steps[0].statistics.cache.missesNo +
                stat.steps[0].statistics.cache.hitsNo;

            lab.expect(cacheCount).to.equal(1);

        });
    });

});<commit_msg>Code cleanup<commit_after>const lab = require("lab").script();
exports.lab = lab;

let Execute = require("../src/index");

lab.experiment("Caching Execution Tree Test", () => {

    lab.test("the result should be {a:1} because of caching", () => {
        let execute = new Execute();

        const func = (data)=> {
            return new Promise((resolve) => {

                setTimeout(()=> {
                    console.log(data);
                    resolve(data);
                }, 0);
            });
        };

        const cacheOptions = {
            enable: true,
            ttl: 60,
            key: (data) => data.sub_id
        };

        let executionTree = Execute.prepareExecutionTree({
            concurrency: 1,
            cache: cacheOptions,
            steps :[
                {
                    title:"step 1",
                    action: (data) => func({a: 1})
                }
            ]
        });

        let executionData = {
            sub_id :123
        };

        return execute.run(executionTree, executionData).then( ()=> {

            executionTree = Execute.prepareExecutionTree({
                concurrency: 1,
                cache: cacheOptions,
                steps :[
                    {
                        title:"step 1",
                        action: (data) => func({a: 2})
                    }
                ]
            });

            return execute.run(executionTree, executionData).then( (result)=> {
                lab.expect(result.a).to.equal(1);
            });

        });
    });

    lab.test("statistics should show hits and misses", () => {
        let execute = new Execute();

        const func = (data)=> {
            return new Promise((resolve) => {

                setTimeout(()=> {
                    console.log(data);
                    resolve(data);
                }, 0);
            });
        };

        let newKey = (Math.random()*10000000).toString();

        const cacheOptions = {
            enable: true,
            ttl: 60,
            key: (data) => newKey + data.sub_id
        };

        let executionTree = Execute.prepareExecutionTree({
            concurrency: 1,
            steps :[
                {
                    cache: cacheOptions,
                    title:"step 1",
                    action: (data) => func({a: 1})
                },
                {
                    cache: cacheOptions,
                    title:"step 2",
                    action: (data) => func({b: 2})
                },
                {
                    cache: cacheOptions,
                    title:"step 3",
                    action: (data) => func({c: 3})
                },
                {
                    cache: cacheOptions,
                    title:"step 4",
                    action: (data) => func({d: 4})
                },
                {
                    cache: cacheOptions,
                    title:"step 5",
                    action: (data) => func({e: 5})
                }
            ]
        });

        let executionData = {
            sub_id :123
        };

        return execute.run(executionTree, executionData).then( (result)=> {
            lab.expect(result.a).to.equal(1);

            let stat = Execute.extractStatistics(executionTree);
            let cacheCount =
                stat.steps[0].statistics.cache.missesNo +
                stat.steps[0].statistics.cache.hitsNo;

            lab.expect(cacheCount).to.equal(1);

        });
    });

});<|endoftext|>