<commit_before>var https = require('https');
var fs = require('fs');
var tbl = require('telegram-bot-api');

var StockAlertBot = new function() {
    var main = this;
    this.telegram = {};
    this.data = {
        "token": ""
        , "users": {
            "1880667": {
                    "stocks":[
                        "GOLD"
                    ]
                }
            }
        , "stockStore": {
            "GOLD": 0.3
        }
    };
    this.chatCheck = function(chatId) {
        if(!main.data.users.hasOwnProperty(chatId)) {
            main.data.users[chatId] = {};
        };

        return main.data.users[chatId];
    };
    this.start = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Welcome " + result.message.from.username + " this is your automated stock bot!\n"
                    + "It uses yahoo finance and it alerts you on a 0.1% change in the stock, be it up or down.\n"
                    + "Please check /help before you try to use the bot."
            }
        );
        return false;
    };
    this.help = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "This bot uses yahoo finance.\n"
                    + "To use this stock alert bot,\n"
                    + "find stock id's on yahoo like so: http://finance.yahoo.com/q?s=eurusd=x\n"
                    + "The stock id is the name in the brackets,\n"
                    + "or part of the url like \"(EURUSD=X)\".\n\n"
                    + "Command list:\n"
                    + "/start - Greeting message\n"
                    + "/help - Show this help window\n"
                    + "/settings - Show your added stocks and other info\n"
                    + "/stockadd - Add a stock to alerts\n"
                    + "/stockremove - Remove a stock from alerts\n"
                    + "/cancel - Cancels any ongoing action\n\n"
                    + "If you have problems with this product, please visit us on https://github.com/Nexination/node-telegram-bot-collection"
            }
        );
        return false;
    };
    this.settings = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        var textSettings = '';
        for(var i in chatSettings) {
            textSettings += '- ' + i + '\n' + JSON.stringify(chatSettings[i]);
        };

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Your settings: \n"
                    + (textSettings !== '' ? textSettings : 'none')
            }
        );
        return false;
    };
    this.stockAdd = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        if(result.message.text.substr(0, 1) === '/') {
            main.telegram.deferAction(result.message.chat.id, main.stockAdd);
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Please input the stock id you wish to add:"
                }
            );
        }
        else {
            var stock = result.message.text.toUpperCase();
            if(!chatSettings.hasOwnProperty('stocks')) {
                chatSettings.stocks = [];
            };
            chatSettings.stocks.push(stock);
            if(!main.data.stockStore.hasOwnProperty(stock)) {
                main.data.stockStore[stock] = 0;
            };
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Stock " + stock + " confirmed."
                }
            );
            main.dataFileAction('save');
        };

        return false;
    };
    this.stockRemove = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        if(result.message.text.substr(0, 1) === '/') {
            main.telegram.deferAction(result.message.chat.id, main.stockRemove);
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Please input the stock id you wish to remove:"
                }
            );
        }
        else {
            if(chatSettings.hasOwnProperty('stocks')) {
                var stockPlace = chatSettings.stocks.indexOf(result.message.text.toUpperCase());
                if(stockPlace !== -1) {
                    var removedItem = chatSettings.stocks.splice(stockPlace, 1);
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": result.message.chat.id
                            , "encodedMessage": "Stock " + JSON.stringify(removedItem) + " removed."
                        }
                    );
                    main.dataFileAction('save');
                }
                else {
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": result.message.chat.id
                            , "encodedMessage": "Stock " + result.message.text + " not found."
                        }
                    );
                };
            };
        };

        return false;
    };
    this.deferredActionCancel = function(result) {
        main.telegram.deferActionRemove(result.message.chat.id);
        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Action cancelled."
            }
        );
        return false;
    };
    this.getStockUpdates = function() {
        var callUrl = 'https://query.yahooapis.com/v1/public/yql?q=select%20Symbol,%20PercentChange%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22YHOO%22${target})&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys';
        var stocksToBeCounted = '';

        for(var stock in main.data.stockStore) {
            stocksToBeCounted += ',%22' + stock + '%22';
        };
        callUrl = callUrl.replace('${target}', stocksToBeCounted);

        https.get(callUrl, main.stockDataHandler).on('error', function(e) {
            console.error(e);
        });
        return false;
    };
    this.stockDataHandler = function(resource) {
        var data = '';
        resource.on('data', function(chunk) {
            data += chunk;
        });
        resource.on('end', function() {
            console.log(data);
            var jsonData = {};
            try {
                jsonData = JSON.parse(data);
            } catch(error) {
                console.log(error);
            };
            if(jsonData.hasOwnProperty('query')) {
                if(jsonData.query.hasOwnProperty('result')) {
                    for(var i = 0; i < jsonData.query.results.quote.length; i += 1) {
                        var quote = jsonData.query.results.quote[i];
                        if(quote.Symbol !== 'YHOO') {
                            if(quote.PercentChange !== null) {
                                var currentQuote = Math.floor(parseFloat(quote.PercentChange.substr(0, quote.PercentChange.length-1)) * 10) / 10;
                                if(main.data.stockStore[quote.Symbol] !== currentQuote) {
                                    console.log(quote.Symbol + ' ' + currentQuote);
                                    main.data.stockStore[quote.Symbol] = currentQuote;
                                    main.alarmUsers(quote.Symbol);
                                    main.dataFileAction('save');
                                };
                            };
                        };
                    };
                };
            };
        });
        return false;
    };
    this.alarmUsers = function(stockId) {
        for(chatId in main.data.users) {
            if(main.data.users[chatId].hasOwnProperty('stocks')) {
                if(main.data.users[chatId].stocks.indexOf(stockId) !== -1) {
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": chatId
                            , "encodedMessage": "!ALERT! " + stockId + " has changed to " + main.data.stockStore[stockId]
                        }
                    );
                };
            };
        };
        return false;
    };
    this.dataFileAction = function(action, runAfter) {
        var dataFile = 'stockdata';
        if(action === 'load') {
            fs.readFile(
                dataFile
                , function(error, data) {
                    if(!error) {
                        main.data = JSON.parse(data);
                        if(typeof runAfter === 'function') {
                            runAfter();
                        };
                    };
                }
            );
        }
        else if(action === 'save') {
            fs.writeFile(
                dataFile
                , JSON.stringify(main.data)
                , function(error, data) {
                    if(error) {
                        console.log(error);
                    }
                    else if(typeof runAfter === 'function') {
                        runAfter();
                    };
                }
            );
        }
        return false;
    };
    this.alertAllUsers = function() {
        var updateFileName = 'update';
        if(fs.existsSync(updateFileName)) {
            fs.readFile(
                'update'
                , function(error, data) {
                    if(!error) {
                        var update = JSON.parse(data);
                        if(update.hasOwnProperty('news')) {
                            for(var i in main.data.users) {
                                main.telegram.apiCall(
                                    'sendMessage'
                                    , {
                                        "chatId": i
                                        , "encodedMessage": update.news
                                    }
                                );
                            };
                        };
                    };
                }
            );
        };
        return false;
    };
    this.runAfterLoad = function() {
        main.telegram = new tbl.TelegramBotLib({"botToken": main.data.token});

        main.telegram.on('start', main.start);
        main.telegram.on('help', main.help);
        main.telegram.on('settings', main.settings);
        main.telegram.on('stockadd', main.stockAdd);
        main.telegram.on('stockremove', main.stockRemove);
        main.telegram.on('cancel', main.deferredActionCancel);

        main.alertAllUsers();
        main.getStockUpdates();
        setInterval(main.getStockUpdates, (5*60*1000));

        return false;
    };
    this.__construct = function() {
        main.dataFileAction('load', main.runAfterLoad);

    };
    this.__construct();
};
<commit_msg>[BUGFIX] Fixed a lot of minor issues and cleaned up some data<commit_after>"use strict";
var https = require('https');
var fs = require('fs');
var tbl = require('telegram-bot-api');

var StockAlertBot = new function() {
    var main = this;
    this.telegram = {};
    this.data = {
        "token": ""
        , "users": {
            "1880667": {
                    "stocks":[
                        "GOLD"
                    ]
                }
            }
        , "stockStore": {
            "GOLD": 0.3
        }
    };
    this.chatCheck = function(chatId) {
        if(!main.data.users.hasOwnProperty(chatId)) {
            main.data.users[chatId] = {};
        };

        return main.data.users[chatId];
    };
    this.start = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Welcome " + result.message.from.username + " this is your automated stock bot!\n"
                    + "It uses yahoo finance and it alerts you on a 0.1% change in the stock, be it up or down.\n"
                    + "Please check /help before you try to use the bot."
            }
        );
        return false;
    };
    this.help = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "This bot uses yahoo finance.\n"
                    + "To use this stock alert bot,\n"
                    + "find stock id's on yahoo like so: http://finance.yahoo.com/q?s=eurusd=x\n"
                    + "The stock id is the name in the brackets,\n"
                    + "or part of the url like \"(EURUSD=X)\".\n\n"
                    + "Command list:\n"
                    + "/start - Greeting message\n"
                    + "/help - Show this help window\n"
                    + "/settings - Show your added stocks and other info\n"
                    + "/stockadd - Add a stock to alerts\n"
                    + "/stockremove - Remove a stock from alerts\n"
                    + "/cancel - Cancels any ongoing action\n\n"
                    + "If you have problems with this product, please visit us on https://github.com/Nexination/node-telegram-bot-collection"
            }
        );
        return false;
    };
    this.settings = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        var textSettings = '';
        for(var i in chatSettings) {
            textSettings += '- ' + i + '\n' + JSON.stringify(chatSettings[i]);
        };

        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Your settings: \n"
                    + (textSettings !== '' ? textSettings : 'none')
            }
        );
        return false;
    };
    this.stockAdd = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        if(result.message.text.substr(0, 1) === '/') {
            main.telegram.deferAction(result.message.chat.id, main.stockAdd);
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Please input the stock id you wish to add:"
                }
            );
        }
        else {
            var stock = result.message.text.toUpperCase();
            if(!chatSettings.hasOwnProperty('stocks')) {
                chatSettings.stocks = [];
            };
            chatSettings.stocks.push(stock);
            if(!main.data.stockStore.hasOwnProperty(stock)) {
                main.data.stockStore[stock] = 0;
            };
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Stock " + stock + " confirmed."
                }
            );
            main.dataFileAction('save');
        };

        return false;
    };
    this.stockRemove = function(result) {
        var chatSettings = main.chatCheck(result.message.chat.id);

        if(result.message.text.substr(0, 1) === '/') {
            main.telegram.deferAction(result.message.chat.id, main.stockRemove);
            main.telegram.apiCall(
                'sendMessage'
                , {
                    "chatId": result.message.chat.id
                    , "encodedMessage": "Please input the stock id you wish to remove:"
                }
            );
        }
        else {
            if(chatSettings.hasOwnProperty('stocks')) {
                var stockPlace = chatSettings.stocks.indexOf(result.message.text.toUpperCase());
                if(stockPlace !== -1) {
                    var removedItem = chatSettings.stocks.splice(stockPlace, 1);
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": result.message.chat.id
                            , "encodedMessage": "Stock " + JSON.stringify(removedItem) + " removed."
                        }
                    );
                    main.dataFileAction('save');
                }
                else {
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": result.message.chat.id
                            , "encodedMessage": "Stock " + result.message.text + " not found."
                        }
                    );
                };
            };
        };

        return false;
    };
    this.deferredActionCancel = function(result) {
        main.telegram.deferActionRemove(result.message.chat.id);
        main.telegram.apiCall(
            'sendMessage'
            , {
                "chatId": result.message.chat.id
                , "encodedMessage": "Action cancelled."
            }
        );
        return false;
    };
    this.getStockUpdates = function() {
        var callUrl = 'https://query.yahooapis.com/v1/public/yql?q=select%20Symbol,%20PercentChange%20from%20yahoo.finance.quotes%20where%20symbol%20in%20(%22YHOO%22${target})&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys';
        var stocksToBeCounted = '';

        main.cleanUpUsers();

        for(var stock in main.data.stockStore) {
            stocksToBeCounted += ',%22' + stock + '%22';
        };
        callUrl = callUrl.replace('${target}', stocksToBeCounted);

        https.get(callUrl, main.stockDataHandler).on('error', function(e) {
            console.error(e);
        });

        return false;
    };
    this.stockDataHandler = function(resource) {
        var data = '';
        resource.on('data', function(chunk) {
            data += chunk;
        });
        resource.on('end', function() {
            var jsonData = {};
            try {
                jsonData = JSON.parse(data);
            } catch(error) {
                console.log(error);
            };
            if(jsonData.hasOwnProperty('query')) {
                if(jsonData.query.hasOwnProperty('results')) {
                    for(var i = 0; i < jsonData.query.results.quote.length; i += 1) {
                        var quote = jsonData.query.results.quote[i];
                        console.log(quote.PercentChange);
                        if(quote.Symbol !== 'YHOO') {
                            if(quote.PercentChange !== null) {
                                var currentQuote = Math.floor(parseFloat(quote.PercentChange.substr(0, quote.PercentChange.length-1)) * 10) / 10;
                                if(main.data.stockStore[quote.Symbol] !== currentQuote) {
                                    console.log(quote.Symbol + ' ' + currentQuote);
                                    main.data.stockStore[quote.Symbol] = currentQuote;
                                    main.alarmUsers(quote.Symbol);
                                };
                            }
                            else {
                                console.log('Deleted ' + quote.Symbol);
                                delete main.data.stockStore[quote.Symbol];
                            };
                        };
                    };
                    main.dataFileAction('save');
                };
            };
        });
        return false;
    };
    this.alarmUsers = function(stockId) {
        for(let chatId in main.data.users) {
            if(main.data.users[chatId].hasOwnProperty('stocks')) {
                if(main.data.users[chatId].stocks.indexOf(stockId) !== -1) {
                    main.telegram.apiCall(
                        'sendMessage'
                        , {
                            "chatId": chatId
                            , "encodedMessage": "!ALERT! " + stockId + " has changed to " + main.data.stockStore[stockId]
                        }
                    );
                };
            };
        };
        return false;
    };
    this.cleanUpUsers = function() {
        for(let chatId in main.data.users) {
            let deleteUser = false;
            if(main.data.users[chatId].hasOwnProperty('stocks')) {
                let stocks = main.data.users[chatId].stocks;
                if(stocks.length === 0) {
                    deleteUser = true;
                }
                else {
                    for(let i = 0; i < stocks.length; i += 1) {
                        if(main.data.stockStore[stocks[i]] === undefined) {
                            stocks.splice(i, 1);
                        };
                    };
                };
            }
            else {
                deleteUser = true;
            };

            if(deleteUser) {
                console.log('Deleting user ' + chatId);
                delete main.data.users[chatId];
            };
        };
        main.dataFileAction('save');
        return false;
    };
    this.dataFileAction = function(action, runAfter) {
        var dataFile = 'stockdata';
        if(action === 'load') {
            fs.readFile(
                dataFile
                , function(error, data) {
                    if(!error) {
                        main.data = JSON.parse(data);
                        if(typeof runAfter === 'function') {
                            runAfter();
                        };
                    };
                }
            );
        }
        else if(action === 'save') {
            fs.writeFile(
                dataFile
                , JSON.stringify(main.data)
                , function(error, data) {
                    if(error) {
                        console.log(error);
                    }
                    else if(typeof runAfter === 'function') {
                        runAfter();
                    };
                }
            );
        }
        return false;
    };
    this.alertAllUsers = function() {
        var updateFileName = 'update';
        if(fs.existsSync(updateFileName)) {
            fs.readFile(
                'update'
                , function(error, data) {
                    if(!error) {
                        var update = JSON.parse(data);
                        if(update.hasOwnProperty('news')) {
                            for(var i in main.data.users) {
                                main.telegram.apiCall(
                                    'sendMessage'
                                    , {
                                        "chatId": i
                                        , "encodedMessage": update.news
                                    }
                                );
                            };
                        };
                    };
                }
            );
        };
        return false;
    };
    this.runAfterLoad = function() {
        main.telegram = new tbl.TelegramBotLib({"botToken": main.data.token});

        main.telegram.on('start', main.start);
        main.telegram.on('help', main.help);
        main.telegram.on('settings', main.settings);
        main.telegram.on('stockadd', main.stockAdd);
        main.telegram.on('stockremove', main.stockRemove);
        main.telegram.on('cancel', main.deferredActionCancel);

        main.alertAllUsers();
        main.getStockUpdates();
        setInterval(main.getStockUpdates, (5*60*1000));

        return false;
    };
    this.__construct = function() {
        main.dataFileAction('load', main.runAfterLoad);

    };
    this.__construct();
};
<|endoftext|>


<commit_before>// How would you reverse a singly LL (no loop)?

function reversesll(sll){

  if(!sll.head || !sll.head.next) return sll; //check if empty

  var nodes = [],
      current = sll.head;

  while(current){ //storing all the nodes in an array
    nodes.push(current);
    current = current.next;
  }

  var reversedLL = new LinkedList();

  reversedLL.head = nodes.pop();
  current = reversedLL.head;

  var node = nodes.pop();
  //make sure to make next of the newly inserted node to be null
  //other wise the last node of your SLL will retain its old next.
  while(node){
     node.next = null;
     current.next = node;

     current = current.next;
     node = nodes.pop();
  }
  return reversedLL;
}

//------------------------------------------------

function reverseList(head) {
    this.prev = null;
    this.curr = head;
    while (this.curr) {
        this.nextTemp = curr.next;
        curr.next = this.prev;
        this.prev = curr;
        curr = nextTemp;
    }
    return this.prev;
}

// ----------------------------------------

// 0(n) iterative
var reverseList = function(head) {
    this.prev = null;
    this.curr = head;
    while (this.curr !== null) {
        var nextTemp = curr.next;
        curr.next = this.prev;
        this.prev = this.curr;
        this.curr = nextTemp;
    }
    return this.prev;
};

// --------------------------------------------
// 0(n) recursive

var reverseList = function(head) {
    if (head === null || head.next === null){
      return head;
    }
    var temp = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return temp;
};

var reverseList = function(head, prev) {
   if(prev === undefined){
       prev = null;
   }
   if(head === null){
       return prev;
   }
   var temp = head.next;
   head.next = prev;
   return reverseList(temp, head);
};
<commit_msg>edit solution<commit_after>// How would you reverse a singly LL (no loop)?

function reversesll(sll){

  if (!sll.head || !sll.head.next) return sll; //check if empty

  const nodes = [];
  let current = sll.head;

  while (current) { //storing all the nodes in an array
    nodes.push(current);
    current = current.next;
  }

  const reversedLL = new LinkedList();

  reversedLL.head = nodes.pop();
  current = reversedLL.head;

  let node = nodes.pop();
  //make sure to make next of the newly inserted node to be null
  //other wise the last node of your SLL will retain its old next.
  while (node) {
    node.next = null;
    current.next = node;

    current = current.next;
    node = nodes.pop();
  }
  return reversedLL;
}

//------------------------------------------------

function reverseList(head) {
    this.prev = null;
    this.curr = head;
    while (this.curr) {
        this.nextTemp = curr.next;
        curr.next = this.prev;
        this.prev = curr;
        curr = nextTemp;
    }
    return this.prev;
}

// ----------------------------------------

// 0(n) iterative
var reverseList = function(head) {
    this.prev = null;
    this.curr = head;
    while (this.curr !== null) {
        var nextTemp = curr.next;
        curr.next = this.prev;
        this.prev = this.curr;
        this.curr = nextTemp;
    }
    return this.prev;
};

// --------------------------------------------
// 0(n) recursive

var reverseList = function(head) {
    if (head === null || head.next === null){
      return head;
    }
    var temp = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return temp;
};

var reverseList = function(head, prev) {
   if(prev === undefined){
       prev = null;
   }
   if(head === null){
       return prev;
   }
   var temp = head.next;
   head.next = prev;
   return reverseList(temp, head);
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import { connect } from 'react-redux';

import gsap from 'gsap';

/* component styles */
import { styles } from './styles.scss';

const emoColors = {
  anger: '#FF3F39',
  sadness: '#2B56B2',
  disgust: '#AC35B2',
  fear: '#4ACC68',
  joy: '#FFF348',
};

const emoDesc = {
  anger: 'anger',
  sadness: 'sadness',
  disgust: 'disgust',
  fear: 'fear',
  joy: 'joy',
};

const socColors = {
  openness: '#FF3F39',
  conscientiousness: '#2B56B2',
  extraversion: '#AC35B2',
  agreeableness: '#4ACC68',
  neuroticism: '#FFF348',
};

const socDesc = {
  openness: 'openness',
  conscientiousness: 'conscientiousness',
  extraversion: 'extraversion',
  agreeableness: 'agreeableness',
  neuroticism: 'neuroticism',
};

class EmotionDisplay extends Component {
  constructor(props) {
    super(props);
    this.state = {
      lastAnger: 0,
      xCoord: 440,
      activeGraph: 'emotion',
      emotionData: [],
      emotionPaths: null,
      emotionKey: {
        anger: 0.15,
        sadness: 0.15,
        disgust: 0.15,
        fear: 0.15,
        joy: 0.15,
      },
      dominantEmo: '',
      socialData: [],
      socialPaths: null,
      socialKey: {
        openness: 0.15,
        conscientiousness: 0.15,
        extraversion: 0.15,
        agreeableness: 0.15,
        neuroticism: 0.15,
      },
      dominantSoc: '',
      topRef: false,
      btmRef: false,
    };
  }

  componentWillMount() {
    // because the incoming data is continuous, need to disable
    // requestAnimationFrame so that moving to a new window doesn't
    // get the data animation out of whack
    TweenMax.ticker.useRAF(false);
    TweenMax.lagSmoothing(0);
  }

  componentWillReceiveProps(props) {
    const currentAnger = props.emotion.anger;
    if (currentAnger !== this.state.lastAnger) {
      const altSocialKeys = {};
      delete props.social.id;
      for (let key in props.social) {
        altSocialKeys[key.split('_')[0]] = props.social[key];
      }

      const newEmotionData = this.state.emotionData.concat([props.emotion]);
      const newSocialData = this.state.socialData.concat([altSocialKeys]);

      if (newEmotionData.length > 30) {
        newEmotionData.shift();
        newSocialData.shift();
      }

      const emoData = this.transformData(newEmotionData, this.state.xCoord);
      const socData = this.transformData(newSocialData, this.state.xCoord);

      this.setState({
        lastAnger: currentAnger,
        xCoord: this.state.xCoord + 40,
        emotionData: newEmotionData,
        emotionPaths: emoData.paths,
        emotionKey: emoData.avgs,
        dominantEmo: emoData.diff.key,
        socialData: newSocialData,
        socialPaths: socData.paths,
        socialKey: socData.avgs,
        dominantSoc: socData.diff.key,
      });

      TweenMax.to('#line-container', 3, { x: '-=40', ease: Power0.easeNone });

      TweenMax.to('#anger-splotch', 0.5, { scale: this.state.emotionKey.anger });
      TweenMax.to('#sadness-splotch', 0.5, { scale: this.state.emotionKey.sadness });
      TweenMax.to('#joy-splotch', 0.5, { scale: this.state.emotionKey.joy });
      TweenMax.to('#fear-splotch', 0.5, { scale: this.state.emotionKey.fear });
      TweenMax.to('#disgust-splotch', 0.5, { scale: this.state.emotionKey.disgust });

      TweenMax.to('#openness-splotch', 0.5, { scale: this.state.socialKey.openness });
      TweenMax.to('#conscientiousness-splotch', 0.5, { scale: this.state.socialKey.conscientiousness });
      TweenMax.to('#extraversion-splotch', 0.5, { scale: this.state.socialKey.extraversion });
      TweenMax.to('#agreeableness-splotch', 0.5, { scale: this.state.socialKey.agreeableness });
      TweenMax.to('#neuroticism-splotch', 0.5, { scale: this.state.socialKey.neuroticism });

      TweenMax.to('#emo-graph-bg', 0.5, { fill: emoColors[emoData.diff.key], fillOpacity: (emoData.diff.magnitude) });
      TweenMax.to('#soc-graph-bg', 0.5, { fill: socColors[socData.diff.key], fillOpacity: (socData.diff.magnitude) });
    }
  }

  transformData(data, xCoord) {
    if (data.length === 0) return;

    const readings = Object.keys(data[0]).reduce((list, reading) => {
      list[reading] = [];
      return list;
    }, {});

    data.forEach(datum => {
      for (let key in datum) {
        if (datum.hasOwnProperty(key)) {
          readings[key].push(datum[key]);
        }
      }
    });

    const transformed = {};
    const paths = {};
    const avgs = {};
    const avgDiff = [];

    for (let key in readings) {
      if (key === 'id') continue;
      if (readings.hasOwnProperty(key)) {
        transformed[key] = readings[key].reduceRight((res, reading, i, coll) => {
          if (i === coll.length - 1) {
            res.path += `${res.x} ${100 - reading}`;
          } else {
            if (reading >= coll[i + 1]) {
              res.path += `C ${res.x + 20} ${100 - (coll[i + 1])} ${res.x + 20} ${100 - (reading)} ${res.x} ${100 - reading}`
            } else {
              res.path += `C ${res.x + 20} ${100 - (coll[i + 1])} ${res.x + 20} ${100 - (reading)} ${res.x} ${100 - reading}`
            }
          }
          res.x -= 40;
          res.avg += (reading / coll.length);
          return res;
        }, { path: 'M', x: xCoord, avg: 0 });
      }
    }

    for (let key in transformed) {
      if (transformed.hasOwnProperty(key)) {
        paths[key] = transformed[key].path;
        avgs[key] = transformed[key].avg;
        avgDiff.push({ key: key, avg: avgs[key] });
        avgs[key] = ((Math.round(avgs[key] * 100) / 100) * 0.0085) + 0.15;
      }
    }

    avgDiff.sort((a, b) => b.avg - a.avg);
    const diff = { key: avgDiff[0].key, magnitude: (avgDiff[0].avg - avgDiff[1].avg) / 100 };

    return { paths, avgs, diff };
  }

  renderSplotches(graphKey, descriptions) {
    if (!graphKey) return;
    const splotches = [];
    for (let splotch in graphKey) {
      splotches.push(

        <span id={`${splotch}-splotch`} className="color-splotch">

          {splotch}

            {descriptions[splotch]}

);
    }
    return (

        {splotches}

    );
  }

  renderLines(colors, paths) {
    if (!paths) return;
    const lines = [];
    for (let key in paths) {
      lines.push(<path key={key} stroke={colors[key]} fill="none" d={paths[key]} />)
    }
    return (
      <g>{lines}</g>
    );
  }

  renderOutlines(paths) {
    if (!paths) return;
    const outlines = [];
    for (let key in paths) {
      outlines.push(<path key={key} stroke="#FFF" className="stroke-outline" fill="none" d={paths[key]} />);
    }
    return (
      <g>{outlines}</g>
    )
  }

  handleMouseEnter(line) {
    if (line === 'top') this.setState({ topRef: true });
    if (line === 'btm') this.setState({ btmRef: true });
  }

  handleMouseLeave(line) {
    if (line === 'top') this.setState({ topRef: false });
    if (line === 'btm') this.setState({ btmRef: false });
  }

  toggleGraph(graph) {
    if (graph === 'social' && this.state.activeGraph === 'emotion') this.setState({ activeGraph: 'social' });
    if (graph === 'emotion' && this.state.activeGraph === 'social') this.setState({ activeGraph: 'emotion' });
  }

  render() {
    return (
      <div className={`${styles}`}>

            <h2
              className={`graph-tab ${this.state.activeGraph === 'emotion' ? 'tab-active' : ''}`}
              onClick={() => this.toggleGraph('emotion')}>
                Channel Emotions

            <h2
              className={`graph-tab ${this.state.activeGraph === 'social' ? 'tab-active' : ''}`}
              onClick={() => this.toggleGraph('social')}>
                Social Attributes

          <section className={`graph-key dom-emo-${this.state.dominantEmo} dom-soc-${this.state.dominantSoc}`}>
            <div className={`row splotch-row ${this.state.activeGraph === 'emotion' ? 'splotch-row-active' : ''}`}>
              {this.renderSplotches(emoColors, emoDesc)}

            <div className={`row splotch-row ${this.state.activeGraph === 'social' ? 'splotch-row-active' : ''}`}>
              {this.renderSplotches(socColors, socDesc)}

              <span className={`graph-explanation ${this.state.topRef ? 'visible' : ''}`}>Values above this line indicate strong emotion
              <span className={`graph-explanation ${this.state.btmRef ? 'visible' : ''}`}>Values below this line indicate weak emotion

              <svg width="100%" height="400" viewBox="0 0 400 100" preserveAspectRatio="none">

                <rect className={`graph-bg ${this.state.activeGraph === 'emotion' ? 'bg-active' : ''}`} id="emo-graph-bg" x="0" y="0" width="400" height="100" fill="#fff" fillOpacity="0" />
                <rect className={`graph-bg ${this.state.activeGraph === 'social' ? 'bg-active' : ''}`} id="soc-graph-bg" x="0" y="0" width="400" height="100" fill="#fff" fillOpacity="0" />

                <path className="reference-line" d="M 0.3 25 l 399.7 0" />
                <rect id="upper-ref" x="0" y="0" width="400" height="25" fill="#fff" className={`ref-box ${this.state.topRef ? 'visible' : ''}`} />

                <path className="reference-line" d="M 0.3 50 l 399.7 0" />
                <rect id="lower-ref" x="0" y="50" width="400" height="50" fill="#fff" className={`ref-box ${this.state.btmRef ? 'visible' : ''}`} />

                <g id="line-container">
                  <g className={`graph-lines ${this.state.activeGraph === 'emotion' ? 'lines-active' : ''}`}>
                    {this.renderOutlines(this.state.emotionPaths)}
                    {this.renderLines(emoColors, this.state.emotionPaths)}
                  </g>
                  <g className={`graph-lines ${this.state.activeGraph === 'social' ? 'lines-active' : ''}`}>
                    {this.renderOutlines(this.state.socialPaths)}
                    {this.renderLines(socColors, this.state.socialPaths)}
                  </g>
                </g>

                <path id="reference-line-top" stroke="transparent" d="M 0.3 25 l 399.7 0" onMouseEnter={() => this.handleMouseEnter('top')} onMouseLeave={() => this.handleMouseLeave('top')} />
                <path id="reference-line-btm" stroke="transparent" d="M 0.3 50 l 399.7 0" onMouseEnter={() => this.handleMouseEnter('btm')} onMouseLeave={() => this.handleMouseLeave('btm')} />

              </svg>
              30 Sec
Ago
              Now

    );
  }
}

function mapStateToProps({ tone }) {
  if (tone.toneData) {
    return {
      emotion: tone.toneData.emotion,
      social: tone.toneData.social,
    };
  }
  return {
    emotion: tone,
    social: tone,
  };
}

export default connect(mapStateToProps)(EmotionDisplay);
<commit_msg>Update emotion headings (#110)<commit_after>import React, { Component } from 'react';
import { connect } from 'react-redux';

import gsap from 'gsap';

/* component styles */
import { styles } from './styles.scss';

const emoColors = {
  anger: '#FF3F39',
  sadness: '#2B56B2',
  disgust: '#AC35B2',
  fear: '#4ACC68',
  joy: '#FFF348',
};

const emoDesc = {
  anger: 'Anger',
  sadness: 'Sadness',
  disgust: 'Disgust',
  fear: 'Fear',
  joy: 'Joy',
};

const socColors = {
  openness: '#FF3F39',
  conscientiousness: '#2B56B2',
  extraversion: '#AC35B2',
  agreeableness: '#4ACC68',
  neuroticism: '#FFF348',
};

const socDesc = {
  openness: 'openness',
  conscientiousness: 'conscientiousness',
  extraversion: 'extraversion',
  agreeableness: 'agreeableness',
  neuroticism: 'neuroticism',
};

class EmotionDisplay extends Component {
  constructor(props) {
    super(props);
    this.state = {
      lastAnger: 0,
      xCoord: 440,
      activeGraph: 'emotion',
      emotionData: [],
      emotionPaths: null,
      emotionKey: {
        anger: 0.15,
        sadness: 0.15,
        disgust: 0.15,
        fear: 0.15,
        joy: 0.15,
      },
      dominantEmo: '',
      socialData: [],
      socialPaths: null,
      socialKey: {
        openness: 0.15,
        conscientiousness: 0.15,
        extraversion: 0.15,
        agreeableness: 0.15,
        neuroticism: 0.15,
      },
      dominantSoc: '',
      topRef: false,
      btmRef: false,
    };
  }

  componentWillMount() {
    // because the incoming data is continuous, need to disable
    // requestAnimationFrame so that moving to a new window doesn't
    // get the data animation out of whack
    TweenMax.ticker.useRAF(false);
    TweenMax.lagSmoothing(0);
  }

  componentWillReceiveProps(props) {
    const currentAnger = props.emotion.anger;
    if (currentAnger !== this.state.lastAnger) {
      const altSocialKeys = {};
      delete props.social.id;
      for (let key in props.social) {
        altSocialKeys[key.split('_')[0]] = props.social[key];
      }

      const newEmotionData = this.state.emotionData.concat([props.emotion]);
      const newSocialData = this.state.socialData.concat([altSocialKeys]);

      if (newEmotionData.length > 30) {
        newEmotionData.shift();
        newSocialData.shift();
      }

      const emoData = this.transformData(newEmotionData, this.state.xCoord);
      const socData = this.transformData(newSocialData, this.state.xCoord);

      this.setState({
        lastAnger: currentAnger,
        xCoord: this.state.xCoord + 40,
        emotionData: newEmotionData,
        emotionPaths: emoData.paths,
        emotionKey: emoData.avgs,
        dominantEmo: emoData.diff.key,
        socialData: newSocialData,
        socialPaths: socData.paths,
        socialKey: socData.avgs,
        dominantSoc: socData.diff.key,
      });

      TweenMax.to('#line-container', 3, { x: '-=40', ease: Power0.easeNone });

      TweenMax.to('#anger-splotch', 0.5, { scale: this.state.emotionKey.anger });
      TweenMax.to('#sadness-splotch', 0.5, { scale: this.state.emotionKey.sadness });
      TweenMax.to('#joy-splotch', 0.5, { scale: this.state.emotionKey.joy });
      TweenMax.to('#fear-splotch', 0.5, { scale: this.state.emotionKey.fear });
      TweenMax.to('#disgust-splotch', 0.5, { scale: this.state.emotionKey.disgust });

      TweenMax.to('#openness-splotch', 0.5, { scale: this.state.socialKey.openness });
      TweenMax.to('#conscientiousness-splotch', 0.5, { scale: this.state.socialKey.conscientiousness });
      TweenMax.to('#extraversion-splotch', 0.5, { scale: this.state.socialKey.extraversion });
      TweenMax.to('#agreeableness-splotch', 0.5, { scale: this.state.socialKey.agreeableness });
      TweenMax.to('#neuroticism-splotch', 0.5, { scale: this.state.socialKey.neuroticism });

      TweenMax.to('#emo-graph-bg', 0.5, { fill: emoColors[emoData.diff.key], fillOpacity: (emoData.diff.magnitude) });
      TweenMax.to('#soc-graph-bg', 0.5, { fill: socColors[socData.diff.key], fillOpacity: (socData.diff.magnitude) });
    }
  }

  transformData(data, xCoord) {
    if (data.length === 0) return;

    const readings = Object.keys(data[0]).reduce((list, reading) => {
      list[reading] = [];
      return list;
    }, {});

    data.forEach(datum => {
      for (let key in datum) {
        if (datum.hasOwnProperty(key)) {
          readings[key].push(datum[key]);
        }
      }
    });

    const transformed = {};
    const paths = {};
    const avgs = {};
    const avgDiff = [];

    for (let key in readings) {
      if (key === 'id') continue;
      if (readings.hasOwnProperty(key)) {
        transformed[key] = readings[key].reduceRight((res, reading, i, coll) => {
          if (i === coll.length - 1) {
            res.path += `${res.x} ${100 - reading}`;
          } else {
            if (reading >= coll[i + 1]) {
              res.path += `C ${res.x + 20} ${100 - (coll[i + 1])} ${res.x + 20} ${100 - (reading)} ${res.x} ${100 - reading}`
            } else {
              res.path += `C ${res.x + 20} ${100 - (coll[i + 1])} ${res.x + 20} ${100 - (reading)} ${res.x} ${100 - reading}`
            }
          }
          res.x -= 40;
          res.avg += (reading / coll.length);
          return res;
        }, { path: 'M', x: xCoord, avg: 0 });
      }
    }

    for (let key in transformed) {
      if (transformed.hasOwnProperty(key)) {
        paths[key] = transformed[key].path;
        avgs[key] = transformed[key].avg;
        avgDiff.push({ key: key, avg: avgs[key] });
        avgs[key] = ((Math.round(avgs[key] * 100) / 100) * 0.0085) + 0.15;
      }
    }

    avgDiff.sort((a, b) => b.avg - a.avg);
    const diff = { key: avgDiff[0].key, magnitude: (avgDiff[0].avg - avgDiff[1].avg) / 100 };

    return { paths, avgs, diff };
  }

  capitalizeFirstLetter(string) {
    return string.charAt(0).toUpperCase() + string.slice(1);
  }

  renderSplotches(graphKey, descriptions) {
    if (!graphKey) return;
    const splotches = [];
    for (let splotch in graphKey) {
      splotches.push(

        <span id={`${splotch}-splotch`} className="color-splotch">

          {this.capitalizeFirstLetter(splotch)}

            {descriptions[splotch]}

);
    }
    return (

        {splotches}

    );
  }

  renderLines(colors, paths) {
    if (!paths) return;
    const lines = [];
    for (let key in paths) {
      lines.push(<path key={key} stroke={colors[key]} fill="none" d={paths[key]} />)
    }
    return (
      <g>{lines}</g>
    );
  }

  renderOutlines(paths) {
    if (!paths) return;
    const outlines = [];
    for (let key in paths) {
      outlines.push(<path key={key} stroke="#FFF" className="stroke-outline" fill="none" d={paths[key]} />);
    }
    return (
      <g>{outlines}</g>
    )
  }

  handleMouseEnter(line) {
    if (line === 'top') this.setState({ topRef: true });
    if (line === 'btm') this.setState({ btmRef: true });
  }

  handleMouseLeave(line) {
    if (line === 'top') this.setState({ topRef: false });
    if (line === 'btm') this.setState({ btmRef: false });
  }

  toggleGraph(graph) {
    if (graph === 'social' && this.state.activeGraph === 'emotion') this.setState({ activeGraph: 'social' });
    if (graph === 'emotion' && this.state.activeGraph === 'social') this.setState({ activeGraph: 'emotion' });
  }

  render() {
    return (
      <div className={`${styles}`}>

            <h2
              className={`graph-tab ${this.state.activeGraph === 'emotion' ? 'tab-active' : ''}`}
              onClick={() => this.toggleGraph('emotion')}>
                Channel Emotions

            <h2
              className={`graph-tab ${this.state.activeGraph === 'social' ? 'tab-active' : ''}`}
              onClick={() => this.toggleGraph('social')}>
                Social Attributes

          <section className={`graph-key dom-emo-${this.state.dominantEmo} dom-soc-${this.state.dominantSoc}`}>
            <div className={`row splotch-row ${this.state.activeGraph === 'emotion' ? 'splotch-row-active' : ''}`}>
              {this.renderSplotches(emoColors, emoDesc)}

            <div className={`row splotch-row ${this.state.activeGraph === 'social' ? 'splotch-row-active' : ''}`}>
              {this.renderSplotches(socColors, socDesc)}

              <span className={`graph-explanation ${this.state.topRef ? 'visible' : ''}`}>Values above this line indicate strong emotion
              <span className={`graph-explanation ${this.state.btmRef ? 'visible' : ''}`}>Values below this line indicate weak emotion

              <svg width="100%" height="400" viewBox="0 0 400 100" preserveAspectRatio="none">

                <rect className={`graph-bg ${this.state.activeGraph === 'emotion' ? 'bg-active' : ''}`} id="emo-graph-bg" x="0" y="0" width="400" height="100" fill="#fff" fillOpacity="0" />
                <rect className={`graph-bg ${this.state.activeGraph === 'social' ? 'bg-active' : ''}`} id="soc-graph-bg" x="0" y="0" width="400" height="100" fill="#fff" fillOpacity="0" />

                <path className="reference-line" d="M 0.3 25 l 399.7 0" />
                <rect id="upper-ref" x="0" y="0" width="400" height="25" fill="#fff" className={`ref-box ${this.state.topRef ? 'visible' : ''}`} />

                <path className="reference-line" d="M 0.3 50 l 399.7 0" />
                <rect id="lower-ref" x="0" y="50" width="400" height="50" fill="#fff" className={`ref-box ${this.state.btmRef ? 'visible' : ''}`} />

                <g id="line-container">
                  <g className={`graph-lines ${this.state.activeGraph === 'emotion' ? 'lines-active' : ''}`}>
                    {this.renderOutlines(this.state.emotionPaths)}
                    {this.renderLines(emoColors, this.state.emotionPaths)}
                  </g>
                  <g className={`graph-lines ${this.state.activeGraph === 'social' ? 'lines-active' : ''}`}>
                    {this.renderOutlines(this.state.socialPaths)}
                    {this.renderLines(socColors, this.state.socialPaths)}
                  </g>
                </g>

                <path id="reference-line-top" stroke="transparent" d="M 0.3 25 l 399.7 0" onMouseEnter={() => this.handleMouseEnter('top')} onMouseLeave={() => this.handleMouseLeave('top')} />
                <path id="reference-line-btm" stroke="transparent" d="M 0.3 50 l 399.7 0" onMouseEnter={() => this.handleMouseEnter('btm')} onMouseLeave={() => this.handleMouseLeave('btm')} />

              </svg>
              30 Sec
Ago
              Now

    );
  }
}

function mapStateToProps({ tone }) {
  if (tone.toneData) {
    return {
      emotion: tone.toneData.emotion,
      social: tone.toneData.social,
    };
  }
  return {
    emotion: tone,
    social: tone,
  };
}

export default connect(mapStateToProps)(EmotionDisplay);
<|endoftext|>


<commit_before>#!/usr/bin/env phantomjs
/***********************************************************
 * Author: @mervinej
 * Licence: MIT
 * Date: 11/27/2013
 *
 *  Run with:
 *
 *  $ phantomjs ./grep.js ./urls.txt ./strings.txt
 *
 *  or
 *
 *  $ phantomjs ./grep.js \
 *     "http://foo.com, http://foo.com/bar" \
 *     "string1, string2"
 *
 *  Note: As a bonus, I left the page timing as well from
 *  the example script I started this from.
 *
 ***********************************************************/

var webpage   = require('webpage');
var system    = require('system');
var fs        = require('fs');
var finished  = 0;
var addresses = [];
var strings   = [];

function usage() {
    console.log('Usage: grep.js <URL(s)>|<URL(s) file> <STRING(s)|STRING(s) file>]');
    phantom.exit();
}

if (system.args.length === 1) {
    usage();
}

function trim(str) {
    return str.replace(/^\s+/,'').replace(/\s+$/,'');
}

// parse urls
if (fs.exists(system.args[1])) {
    fs.read(system.args[1])
        .split('\n')
        .forEach(function(line) {
            if (line !== '') {
                addresses.push(line);
            }
        });
} else {
    system.args[1].split(',').forEach(function(item) {
        addresses.push(trim(item));
    });
}

if (system.args[2]) {
    if (fs.exists(system.args[2])) {
        fs.read(system.args[2])
            .split('\n')
            .forEach(function(line) {
                if (line !== '') {
                    strings.push(line);
                }
            });
    } else {
        system.args[2].split(',').forEach(function(item) {
            strings.push(trim(item));
        });
    }
}

if (!addresses || addresses.length === 0) {
    usage();
}

addresses.forEach(function(address) {
    var t = Date.now();
    var page = webpage.create();
    var requests = [];
    page.open(address, function (status) {
        if (status !== 'success') {
            console.log('FAIL to load the address');
            finished++;
            return;
        }
        t = Date.now() - t;

        console.log('Regarding: ' + address);
        console.log('> took ' + t + ' msec');
        console.log(' ');

        var body = page.evaluate(function() {
            return document.body.innerHTML;
        });

        console.log('Search Results:');
        strings.forEach(function(str) {
            var count = 0;
            try {
                count = Object.keys(body.match(new RegExp(str, 'g'))).length;
            } catch(e) {}
            console.log('- ' + str + ': ' + count);
        });

        console.log(' ');
        finished++;

        if (finished === addresses.length) {
            phantom.exit();
        }
        return;
    });

});
<commit_msg>Making case insensitive and only displaying strings that are matched.<commit_after>#!/usr/bin/env phantomjs
/***********************************************************
 * Author: @mervinej
 * Licence: MIT
 * Date: 11/27/2013
 *
 *  Run with:
 *
 *  $ phantomjs ./grep.js ./urls.txt ./strings.txt
 *
 *  or
 *
 *  $ phantomjs ./grep.js \
 *     "http://foo.com, http://foo.com/bar" \
 *     "string1, string2"
 *
 *  Warning: all search are case insensitive.
 *
 *  Note: As a bonus, I left the page timing as well from
 *  the example script I started this from.
 *
 ***********************************************************/

var webpage   = require('webpage');
var system    = require('system');
var fs        = require('fs');
var finished  = 0;
var addresses = [];
var strings   = [];

function usage() {
    console.log('Usage: grep.js <URL(s)>|<URL(s) file> <STRING(s)|STRING(s) file>]');
    phantom.exit();
}

if (system.args.length === 1) {
    usage();
}

function trim(str) {
    return str.replace(/^\s+/,'').replace(/\s+$/,'');
}

// parse urls
if (fs.exists(system.args[1])) {
    fs.read(system.args[1])
        .split('\n')
        .forEach(function(line) {
            if (line !== '') {
                addresses.push(line);
            }
        });
} else {
    system.args[1].split(',').forEach(function(item) {
        addresses.push(trim(item));
    });
}

if (system.args[2]) {
    if (fs.exists(system.args[2])) {
        fs.read(system.args[2])
            .split('\n')
            .forEach(function(line) {
                if (line !== '') {
                    strings.push(line);
                }
            });
    } else {
        system.args[2].split(',').forEach(function(item) {
            strings.push(trim(item));
        });
    }
}

if (!addresses || addresses.length === 0) {
    usage();
}

addresses.forEach(function(address) {
    var t = Date.now();
    var page = webpage.create();
    var requests = [];
    page.open(address, function (status) {
        if (status !== 'success') {
            console.log('FAIL to load the address');
            finished++;
            return;
        }
        t = Date.now() - t;

        console.log('Regarding: ' + address);
        console.log('> took ' + t + ' msec');
        console.log(' ');

        var body = page.evaluate(function() {
            return document.body.innerHTML;
        });

        console.log('Found:');
        strings.forEach(function(str) {
            var count = 0;
            try {
                count = Object.keys(body.match(new RegExp(str, 'ig'))).length;
            } catch(e) {}

            if (count > 0) {
                console.log('- ' + str + ': ' + count);
            }
        });

        console.log(' ');
        finished++;

        if (finished === addresses.length) {
            phantom.exit();
        }
        return;
    });

});
<|endoftext|>


<commit_before>'use strict';

module.exports = function (grunt) {

  require('load-grunt-tasks')(grunt);

  // Default task.
  grunt.registerTask('default', ['jshint', 'karma:unit']);
  grunt.registerTask('serve', ['karma:continuous', 'dist', 'build:gh-pages', 'connect:continuous', 'watch']);
  grunt.registerTask('dist', ['ngAnnotate', 'uglify']);

  // HACK TO ACCESS TO THE COMPONENT-PUBLISHER
  function fakeTargetTask(prefix){
    return function(){

      if (this.args.length !== 1) return grunt.log.fail('Just give the name of the ' + prefix + ' you want like :\ngrunt ' + prefix + ':bower');

      var done = this.async();
      var spawn = require('child_process').spawn;
      spawn('./node_modules/.bin/gulp', [ prefix, '--branch='+this.args[0] ].concat(grunt.option.flags()), {
        cwd : './node_modules/angular-ui-publisher',
        stdio: 'inherit'
      }).on('close', done);
    };
  }

  grunt.registerTask('build', fakeTargetTask('build'));
  grunt.registerTask('publish', fakeTargetTask('publish'));

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    meta: {
      banner: ['/**',
        ' * <%= pkg.name %> - <%= pkg.description %>',
        ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
        ' * @link <%= pkg.homepage %>',
        ' * @license <%= pkg.license %>',
        ' */',
        ''].join('\n')
    },

    watch: {
      src: {
        files: ['src/*'],
        tasks: ['jshint:src', 'karma:unit:run', 'dist', 'build:gh-pages']
      },
      test: {
        files: ['test/*.js'],
        tasks: ['jshint:test', 'karma:unit:run']
      },
      demo: {
        files: ['demo/*', 'publish.js'],
        tasks: ['jshint', 'build:gh-pages']
      },
      livereload: {
        files: ['out/built/gh-pages/**/*'],
        options: { livereload: true }
      }
    },

    karma: {
      unit: {configFile: 'test/karma.conf.js', singleRun: true},
      coverage : {
        configFile: 'test/karma.conf.js',
        reporters: ['progress', 'coverage'],
        preprocessors: { 'src/*.js': ['coverage'] },
        coverageReporter: { type : 'html', dir : 'coverage/' },
        singleRun: true
      },
      server: {configFile: 'test/karma.conf.js'},
      continuous: {configFile: 'test/karma.conf.js',  background: true }
    },

    connect: {
      options: {
        base : 'out/built/gh-pages',
        open: true,
        livereload: true
      },
      server: { options: { keepalive: true } },
      continuous: { options: { keepalive: false } }
    },

    jshint: {
      src: {
        files:{ src : ['src/*.js', 'demo/**/*.js'] },
        options: { jshintrc: '.jshintrc' }
      },
      test: {
        files:{ src : [ 'test/*.spec.js', 'gruntFile.js'] },
        options: grunt.util._.extend({}, grunt.file.readJSON('.jshintrc'), {
          node: true,
          globals: {
            angular: false,
            inject: false,
            jQuery: false,

            jasmine: false,
            afterEach: false,
            beforeEach: false,
            ddescribe: false,
            describe: false,
            expect: false,
            iit: false,
            it: false,
            spyOn: false,
            xdescribe: false,
            xit: false
          }
        })
      }
    },

    uglify: {
      options: {banner: '<%= meta.banner %>'},
      build: {
        expand: true,
        cwd: 'dist',
        src: ['*.js'],
        ext: '.min.js',
        dest: 'dist'
      }
    },

    ngAnnotate: {
      main: {
        expand: true,
        cwd: 'src',
        src: ['*.js'],
        dest: 'dist'
      }
    },

    changelog: {
      options: {
        dest: 'CHANGELOG.md'
      }
    }
  });

};
<commit_msg>chore(grunt): remove irrelevant watch tasks.<commit_after>'use strict';

module.exports = function (grunt) {

  require('load-grunt-tasks')(grunt);

  // Default task.
  grunt.registerTask('default', ['jshint', 'karma:unit']);
  grunt.registerTask('serve', ['karma:continuous', 'dist', 'build:gh-pages', 'connect:continuous', 'watch']);
  grunt.registerTask('dist', ['ngAnnotate', 'uglify']);

  // HACK TO ACCESS TO THE COMPONENT-PUBLISHER
  function fakeTargetTask(prefix){
    return function(){

      if (this.args.length !== 1) return grunt.log.fail('Just give the name of the ' + prefix + ' you want like :\ngrunt ' + prefix + ':bower');

      var done = this.async();
      var spawn = require('child_process').spawn;
      spawn('./node_modules/.bin/gulp', [ prefix, '--branch='+this.args[0] ].concat(grunt.option.flags()), {
        cwd : './node_modules/angular-ui-publisher',
        stdio: 'inherit'
      }).on('close', done);
    };
  }

  grunt.registerTask('build', fakeTargetTask('build'));
  grunt.registerTask('publish', fakeTargetTask('publish'));

  // Project configuration.
  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    meta: {
      banner: ['/**',
        ' * <%= pkg.name %> - <%= pkg.description %>',
        ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>',
        ' * @link <%= pkg.homepage %>',
        ' * @license <%= pkg.license %>',
        ' */',
        ''].join('\n')
    },

    watch: {
      src: {
        files: ['src/*'],
        tasks: ['karma:unit:run', 'dist', 'build:gh-pages']
      },
      test: {
        files: ['test/*.js'],
        tasks: ['karma:unit:run']
      },
      demo: {
        files: ['demo/*', 'publish.js'],
        tasks: ['build:gh-pages']
      },
      livereload: {
        files: ['out/built/gh-pages/**/*'],
        options: { livereload: true }
      }
    },

    karma: {
      unit: {configFile: 'test/karma.conf.js', singleRun: true},
      coverage : {
        configFile: 'test/karma.conf.js',
        reporters: ['progress', 'coverage'],
        preprocessors: { 'src/*.js': ['coverage'] },
        coverageReporter: { type : 'html', dir : 'coverage/' },
        singleRun: true
      },
      server: {configFile: 'test/karma.conf.js'},
      continuous: {configFile: 'test/karma.conf.js',  background: true }
    },

    connect: {
      options: {
        base : 'out/built/gh-pages',
        open: true,
        livereload: true
      },
      server: { options: { keepalive: true } },
      continuous: { options: { keepalive: false } }
    },

    jshint: {
      src: {
        files:{ src : ['src/*.js', 'demo/**/*.js'] },
        options: { jshintrc: '.jshintrc' }
      },
      test: {
        files:{ src : [ 'test/*.spec.js', 'gruntFile.js'] },
        options: grunt.util._.extend({}, grunt.file.readJSON('.jshintrc'), {
          node: true,
          globals: {
            angular: false,
            inject: false,
            jQuery: false,

            jasmine: false,
            afterEach: false,
            beforeEach: false,
            ddescribe: false,
            describe: false,
            expect: false,
            iit: false,
            it: false,
            spyOn: false,
            xdescribe: false,
            xit: false
          }
        })
      }
    },

    uglify: {
      options: {banner: '<%= meta.banner %>'},
      build: {
        expand: true,
        cwd: 'dist',
        src: ['*.js'],
        ext: '.min.js',
        dest: 'dist'
      }
    },

    ngAnnotate: {
      main: {
        expand: true,
        cwd: 'src',
        src: ['*.js'],
        dest: 'dist'
      }
    },

    changelog: {
      options: {
        dest: 'CHANGELOG.md'
      }
    }
  });

};
<|endoftext|>


<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * 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.
 */

import objectAssign from "object-assign";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { Subject } from "rxjs/Subject";
import { Observable } from "rxjs/Observable";

import config from "../../config.js";
import assert from "../../utils/assert.js";
// import takeFirstSet from "../../utils/takeFirstSet.js";

import BandwidthEstimator from "./bandwidth_estimator.js";
import filterByWidth from "./filterByWidth.js";
import filterByBitrate from "./filterByBitrate.js";
import fromBitrateCeil from "./fromBitrateCeil.js";
import EWMA from "./ewma.js";

const {
  ABR_STARVATION_GAP,
  OUT_OF_STARVATION_GAP,
  ABR_STARVATION_FACTOR,
  ABR_REGULAR_FACTOR,
} = config;

/**
 * Returns an observable emitting only the representation concerned by the
 * bitrate ceil given.
 * @param {Array.<Representation>} representations
 * @param {Number} bitrate
 * @returns {Observable}
 */
const setManualRepresentation = (representations, bitrate) => {
  const chosenRepresentation =
    fromBitrateCeil(representations, bitrate) ||
    representations[0];

  return Observable.of({
    bitrate: undefined, // Bitrate estimation is deactivated here
    representation: chosenRepresentation,
  }).concat(Observable.never());
};

/**
 * Get the pending request containing the asked segment position.
 * @param {Object} requests
 * @param {Number} segmentPosition
 * @returns {Object|undefined}
 */
const getConcernedRequest = (requests, segmentPosition) => {
  const currentRequestIds = Object.keys(requests);
  const len = currentRequestIds.length;

  for (let i = 0; i < len - 1; i++) {
    const request = requests[currentRequestIds[i]];
    const {
      time: chunkTime,
      duration: chunkDuration,
    } = request;

    // TODO review this
    if (Math.abs(segmentPosition - chunkTime) < chunkDuration) {
      return request;
    }
  }
};

/**
 * Estimate the __VERY__ recent bandwidth based on a single unfinished request.
 * Useful when the current bandwidth seemed to have fallen quickly.
 *
 * Use progress events if available, set a much more random lower bitrate
 * if no progress events are available.
 *
 * @param {Object} request
 * @param {Number} requestTime - Amount of time the request has taken for now,
 * in seconds.
 * @param {Number} bitrate - Current bitrate at the time of download
 */
const estimateRequestBandwidth = (request, requestTime, bitrate) => {
  let estimate;
  const chunkDuration = request.duration;

  // try to infer quickly the current bitrate based on the
  // progress events
  if (request.progress.length >= 2) {
    const ewma1 = new EWMA(2);

    const { progress } = request;
    for (let i = 1; i < progress.length; i++) {
      const bytesDownloaded =
        progress[i].size - progress[i-1].size;

      const timeElapsed =
        progress[i].timestamp - progress[i-1].timestamp;

      const bitrate =
        (bytesDownloaded * 8) / (timeElapsed / 1000);

      ewma1.addSample(timeElapsed / 1000, bitrate);
    }
    estimate = ewma1.getEstimate();
  }

  // if that fails / no progress event, take a guess
  if (!estimate) {
    const chunkSize = chunkDuration * bitrate;

    // take current duration of request as a base
    estimate = chunkSize / (requestTime * 5/4);
  }
  return estimate;
};

/**
 * Filter representations given through filters options.
 * @param {Array.<Representation>} representations
 * @param {Object} filters
 * @param {Number} [filters.bitrate] - max bitrate authorized (included).
 * @param {Number} [filters.width] - max width authorized (included).
 * @returns {Array.<Representation>}
 */
const getFilteredRepresentations = (representations, filters) => {
  let _representations = representations;

  if (filters.hasOwnProperty("bitrate")) {
    _representations = filterByBitrate(
      _representations, filters.bitrate);
  }

  if (filters.hasOwnProperty("width")) {
    _representations = filterByWidth(
      _representations, filters.width);
  }

  return _representations;
};

/**
 * Returns true if the request takes too much time relatively to how much we
 * should actually wait.
 * Depends on the chunk duration.
 * @param {Number} durationOfRequest - time, in s, since the request has been
 * performed.
 * @param {Number} chunkDuration - duration, in s, of a single chunk
 * @returns {Boolean}
 */
const requestTakesTime = (durationOfRequest, chunkDuration) => {
  return durationOfRequest > 1 + chunkDuration * 1.2;
};

/**
 * Choose the right representation based on multiple parameters given, such as:
 *   - the current user's bandwidth
 *   - the max bitrate authorized
 *   - the size of the video element
 *   - etc.
 *
 * Those parameters can be set through different subjects and methods.
 * The subjects (undocumented here are):
 *
 *   - manualBitrate$ {Subject}: Set the bitrate manually, if no representation
 *     is found with the given bitrate. An immediately inferior one will be
 *     taken instead. If still, none are found, the representation with the
 *     minimum bitrate will be taken.
 *     Set it to a negative value to go into automatic bitrate mode.
 *
 *   - maxBitrate$ {Subject}: Set the maximum automatic bitrate. If the manual
 *     bitrate is not set / set to a negative value, this will be the maximum
 *     switch-able bitrate. If no representation is found inferior or equal to
 *     this bitrate, the representation with the minimum bitrate will be taken.
 *
 */
export default class RepresentationChooser {
  /**
   * @param {Object} options
   * @param {Number} [options.manualBitrate=-1]
   * @param {Number} [options.maxAutoBitrate=Infinity]
   * @param {Number} [options.initialBitrate=0]
   * @param {Observable} [options.limitWidth$]
   * @param {Observable} [options.throttle$]
   */
  constructor(options = {}) {
    this._dispose$ = new Subject();

    this.manualBitrate$ = new BehaviorSubject(
      options.manualBitrate != null ?
        options.manualBitrate : -1
    ).takeUntil(this._dispose$);

    this.maxAutoBitrate$ = new BehaviorSubject(
      options.maxAutoBitrate != null ?
        options.maxAutoBitrate : Infinity
    ).takeUntil(this._dispose$);

    this.estimator = new BandwidthEstimator();
    this._currentRequests = {};

    this.initialBitrate = options.initialBitrate || 0;

    this._limitWidth$ = options.limitWidth$;
    this._throttle$ = options.throttle$;
  }

  get$(clock$, representations) {
    if (representations.length < 2) {
      return Observable.of({
        bitrate: undefined, // Bitrate estimation is deactivated here
        representation: representations.length ?
          representations[0] : null,
      }).concat(Observable.never())
      .takeUntil(this._dispose$);
    }

    const {
      manualBitrate$,
      maxAutoBitrate$,
      initialBitrate,
    }  = this;

    const _deviceEventsArray = [];

    if (this._limitWidth$) {
      _deviceEventsArray.push(this._limitWidth$
        .map(width => ({ width })));
    }

    if (this._throttle$) {
      _deviceEventsArray.push(this._throttle$
        .map(bitrate => ({ bitrate })));
    }

    const deviceEvents$ = _deviceEventsArray.length ?
      Observable.combineLatest(..._deviceEventsArray)
        .map((args) => objectAssign({}, ...args)) : Observable.of({});

    let lastEstimatedBitrate;
    return manualBitrate$.switchMap(manualBitrate => {
      if (manualBitrate >= 0) {
        // MANUAL mode
        return setManualRepresentation(representations, manualBitrate);
      }

      // AUTO mode
      let inStarvationMode = false;
      return Observable.combineLatest(clock$, maxAutoBitrate$, deviceEvents$)
        .map(([ clock, maxAutoBitrate, deviceEvents ]) => {

          let nextBitrate;
          let bandwidthEstimate;
          const { bufferGap } = clock;

          // Check for starvation == not much left to play
          if (bufferGap <= ABR_STARVATION_GAP) {
            inStarvationMode = true;
          } else if (inStarvationMode && bufferGap >= OUT_OF_STARVATION_GAP) {
            inStarvationMode = false;
          }

          // If in starvation mode, check if the request for the next segment
          // takes too much time relatively to the chunk's duration.
          // If that's the case, re-calculate the bandwidth urgently based on
          // this single request.
          if (inStarvationMode) {
            const {
              position,
              bitrate,
            } = clock;

            const nextSegmentPosition = bufferGap + position;
            const request = getConcernedRequest(
              this._currentRequests, nextSegmentPosition);

            if (request) {
              const {
                duration: chunkDuration,
                requestTimestamp,
              } = request;

              const now = Date.now();
              const requestTimeInSeconds = (now - requestTimestamp) / 1000;
              if (
                chunkDuration &&
                requestTakesTime(requestTimeInSeconds, chunkDuration)
              ) {
                bandwidthEstimate = estimateRequestBandwidth(
                  request, requestTimeInSeconds, bitrate);

                if (bandwidthEstimate != null) {
                  // Reset all estimations to zero
                  // Note: this is weird to do this type of "global" side effect
                  // (for this class) in an observable, not too comfortable with
                  // that.
                  this.resetEstimate();
                  nextBitrate = Math.min(
                    bandwidthEstimate,
                    bitrate,
                    maxAutoBitrate
                  );
                }
              }
            }
          }

          // if nextBitrate is not yet defined, do the normal estimation
          if (nextBitrate == null) {
            bandwidthEstimate = this.estimator.getEstimate();

            let nextEstimate;
            if (bandwidthEstimate != null) {
              nextEstimate = clock.bufferGap <= inStarvationMode ?
                bandwidthEstimate * ABR_STARVATION_FACTOR :
                bandwidthEstimate * ABR_REGULAR_FACTOR;
            } else if (lastEstimatedBitrate != null) {
              nextEstimate =  clock.bufferGap <= inStarvationMode ?
                lastEstimatedBitrate * ABR_STARVATION_FACTOR :
                lastEstimatedBitrate * ABR_REGULAR_FACTOR;
            } else {
              nextEstimate = initialBitrate;
            }
            nextBitrate = Math.min(nextEstimate, maxAutoBitrate);
          }

          if (clock.speed > 1) {
            nextBitrate /= clock.speed;
          }

          const _representations =
            getFilteredRepresentations(representations, deviceEvents);

          return {
            bitrate: bandwidthEstimate,
            representation: fromBitrateCeil(_representations, nextBitrate) ||
            representations[0],
          };

        }).do(({ bitrate }) => {
          if (bitrate != null) {
            lastEstimatedBitrate = bitrate;
          }
        }).share();
    });
  }

  /**
   * Add a bandwidth estimate by giving:
   *   - the duration of the request, in s
   *   - the size of the request in bytes
   * @param {Number} duration
   * @param {Number} size
   */
  addEstimate(duration, size) {
    if (duration != null && size != null) {
      this.estimator.addSample(duration, size);
    }
  }

  /**
   * Reset all the estimates done until now.
   * Useful when the network situation changed completely.
   */
  resetEstimate() {
    this.estimator.reset();
  }

  /**
   * Add informations about a new pending request.
   * This can be useful if the network bandwidth drastically changes to infer
   * a new bandwidth through this single request.
   * @param {string|Number} id
   * @param {Segment} segment
   */
  addPendingRequest(id, segment) {
    if (__DEV__) {
      assert(!this._currentRequests[id], "request already added");
    }
    this._currentRequests[id] = segment;
    this._currentRequests[id].progress = [];
  }

  /**
   * Add progress informations to a pending request.
   * Progress objects are a key part to calculate the bandwidth from a single
   * request, in the case the user's bandwidth changes drastically while doing
   * it.
   * @param {string|Number} id
   * @param {Object} progress
   */
  addRequestProgress(id, progress) {
    if (__DEV__) {
      assert(this._currentRequests[id] &&
        this._currentRequests[id].progress, "not a valid request");
    }
    this._currentRequests[id].progress.push(progress);
  }

  /**
   * Remove a request previously set as pending through the addPendingRequest
   * method.
   * @param {string|Number} id
   */
  removePendingRequest(id) {
    if (__DEV__) {
      assert(this._currentRequests[id], "can't remove request: id not found");
    }
    delete this._currentRequests[id];
  }

  /**
   * Remove informations about all pending requests.
   */
  resetRequests() {
    this._currentRequests = {};
  }

  /**
   * TODO Not really needed for now
   */
  dispose() {
    this._dispose$.next();
  }
}
<commit_msg>abr: fix typo<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * 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.
 */

import objectAssign from "object-assign";
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import { Subject } from "rxjs/Subject";
import { Observable } from "rxjs/Observable";

import config from "../../config.js";
import assert from "../../utils/assert.js";
// import takeFirstSet from "../../utils/takeFirstSet.js";

import BandwidthEstimator from "./bandwidth_estimator.js";
import filterByWidth from "./filterByWidth.js";
import filterByBitrate from "./filterByBitrate.js";
import fromBitrateCeil from "./fromBitrateCeil.js";
import EWMA from "./ewma.js";

const {
  ABR_STARVATION_GAP,
  OUT_OF_STARVATION_GAP,
  ABR_STARVATION_FACTOR,
  ABR_REGULAR_FACTOR,
} = config;

/**
 * Returns an observable emitting only the representation concerned by the
 * bitrate ceil given.
 * @param {Array.<Representation>} representations
 * @param {Number} bitrate
 * @returns {Observable}
 */
const setManualRepresentation = (representations, bitrate) => {
  const chosenRepresentation =
    fromBitrateCeil(representations, bitrate) ||
    representations[0];

  return Observable.of({
    bitrate: undefined, // Bitrate estimation is deactivated here
    representation: chosenRepresentation,
  }).concat(Observable.never());
};

/**
 * Get the pending request containing the asked segment position.
 * @param {Object} requests
 * @param {Number} segmentPosition
 * @returns {Object|undefined}
 */
const getConcernedRequest = (requests, segmentPosition) => {
  const currentRequestIds = Object.keys(requests);
  const len = currentRequestIds.length;

  for (let i = 0; i < len - 1; i++) {
    const request = requests[currentRequestIds[i]];
    const {
      time: chunkTime,
      duration: chunkDuration,
    } = request;

    // TODO review this
    if (Math.abs(segmentPosition - chunkTime) < chunkDuration) {
      return request;
    }
  }
};

/**
 * Estimate the __VERY__ recent bandwidth based on a single unfinished request.
 * Useful when the current bandwidth seemed to have fallen quickly.
 *
 * Use progress events if available, set a much more random lower bitrate
 * if no progress events are available.
 *
 * @param {Object} request
 * @param {Number} requestTime - Amount of time the request has taken for now,
 * in seconds.
 * @param {Number} bitrate - Current bitrate at the time of download
 */
const estimateRequestBandwidth = (request, requestTime, bitrate) => {
  let estimate;
  const chunkDuration = request.duration;

  // try to infer quickly the current bitrate based on the
  // progress events
  if (request.progress.length >= 2) {
    const ewma1 = new EWMA(2);

    const { progress } = request;
    for (let i = 1; i < progress.length; i++) {
      const bytesDownloaded =
        progress[i].size - progress[i-1].size;

      const timeElapsed =
        progress[i].timestamp - progress[i-1].timestamp;

      const bitrate =
        (bytesDownloaded * 8) / (timeElapsed / 1000);

      ewma1.addSample(timeElapsed / 1000, bitrate);
    }
    estimate = ewma1.getEstimate();
  }

  // if that fails / no progress event, take a guess
  if (!estimate) {
    const chunkSize = chunkDuration * bitrate;

    // take current duration of request as a base
    estimate = chunkSize / (requestTime * 5/4);
  }
  return estimate;
};

/**
 * Filter representations given through filters options.
 * @param {Array.<Representation>} representations
 * @param {Object} filters
 * @param {Number} [filters.bitrate] - max bitrate authorized (included).
 * @param {Number} [filters.width] - max width authorized (included).
 * @returns {Array.<Representation>}
 */
const getFilteredRepresentations = (representations, filters) => {
  let _representations = representations;

  if (filters.hasOwnProperty("bitrate")) {
    _representations = filterByBitrate(
      _representations, filters.bitrate);
  }

  if (filters.hasOwnProperty("width")) {
    _representations = filterByWidth(
      _representations, filters.width);
  }

  return _representations;
};

/**
 * Returns true if the request takes too much time relatively to how much we
 * should actually wait.
 * Depends on the chunk duration.
 * @param {Number} durationOfRequest - time, in s, since the request has been
 * performed.
 * @param {Number} chunkDuration - duration, in s, of a single chunk
 * @returns {Boolean}
 */
const requestTakesTime = (durationOfRequest, chunkDuration) => {
  return durationOfRequest > 1 + chunkDuration * 1.2;
};

/**
 * Choose the right representation based on multiple parameters given, such as:
 *   - the current user's bandwidth
 *   - the max bitrate authorized
 *   - the size of the video element
 *   - etc.
 *
 * Those parameters can be set through different subjects and methods.
 * The subjects (undocumented here are):
 *
 *   - manualBitrate$ {Subject}: Set the bitrate manually, if no representation
 *     is found with the given bitrate. An immediately inferior one will be
 *     taken instead. If still, none are found, the representation with the
 *     minimum bitrate will be taken.
 *     Set it to a negative value to go into automatic bitrate mode.
 *
 *   - maxBitrate$ {Subject}: Set the maximum automatic bitrate. If the manual
 *     bitrate is not set / set to a negative value, this will be the maximum
 *     switch-able bitrate. If no representation is found inferior or equal to
 *     this bitrate, the representation with the minimum bitrate will be taken.
 *
 */
export default class RepresentationChooser {
  /**
   * @param {Object} options
   * @param {Number} [options.manualBitrate=-1]
   * @param {Number} [options.maxAutoBitrate=Infinity]
   * @param {Number} [options.initialBitrate=0]
   * @param {Observable} [options.limitWidth$]
   * @param {Observable} [options.throttle$]
   */
  constructor(options = {}) {
    this._dispose$ = new Subject();

    this.manualBitrate$ = new BehaviorSubject(
      options.manualBitrate != null ?
        options.manualBitrate : -1
    ).takeUntil(this._dispose$);

    this.maxAutoBitrate$ = new BehaviorSubject(
      options.maxAutoBitrate != null ?
        options.maxAutoBitrate : Infinity
    ).takeUntil(this._dispose$);

    this.estimator = new BandwidthEstimator();
    this._currentRequests = {};

    this.initialBitrate = options.initialBitrate || 0;

    this._limitWidth$ = options.limitWidth$;
    this._throttle$ = options.throttle$;
  }

  get$(clock$, representations) {
    if (representations.length < 2) {
      return Observable.of({
        bitrate: undefined, // Bitrate estimation is deactivated here
        representation: representations.length ?
          representations[0] : null,
      }).concat(Observable.never())
      .takeUntil(this._dispose$);
    }

    const {
      manualBitrate$,
      maxAutoBitrate$,
      initialBitrate,
    }  = this;

    const _deviceEventsArray = [];

    if (this._limitWidth$) {
      _deviceEventsArray.push(this._limitWidth$
        .map(width => ({ width })));
    }

    if (this._throttle$) {
      _deviceEventsArray.push(this._throttle$
        .map(bitrate => ({ bitrate })));
    }

    const deviceEvents$ = _deviceEventsArray.length ?
      Observable.combineLatest(..._deviceEventsArray)
        .map((args) => objectAssign({}, ...args)) : Observable.of({});

    let lastEstimatedBitrate;
    return manualBitrate$.switchMap(manualBitrate => {
      if (manualBitrate >= 0) {
        // MANUAL mode
        return setManualRepresentation(representations, manualBitrate);
      }

      // AUTO mode
      let inStarvationMode = false;
      return Observable.combineLatest(clock$, maxAutoBitrate$, deviceEvents$)
        .map(([ clock, maxAutoBitrate, deviceEvents ]) => {

          let nextBitrate;
          let bandwidthEstimate;
          const { bufferGap } = clock;

          // Check for starvation == not much left to play
          if (bufferGap <= ABR_STARVATION_GAP) {
            inStarvationMode = true;
          } else if (inStarvationMode && bufferGap >= OUT_OF_STARVATION_GAP) {
            inStarvationMode = false;
          }

          // If in starvation mode, check if the request for the next segment
          // takes too much time relatively to the chunk's duration.
          // If that's the case, re-calculate the bandwidth urgently based on
          // this single request.
          if (inStarvationMode) {
            const {
              position,
              bitrate,
            } = clock;

            const nextSegmentPosition = bufferGap + position;
            const request = getConcernedRequest(
              this._currentRequests, nextSegmentPosition);

            if (request) {
              const {
                duration: chunkDuration,
                requestTimestamp,
              } = request;

              const now = Date.now();
              const requestTimeInSeconds = (now - requestTimestamp) / 1000;
              if (
                chunkDuration &&
                requestTakesTime(requestTimeInSeconds, chunkDuration)
              ) {
                bandwidthEstimate = estimateRequestBandwidth(
                  request, requestTimeInSeconds, bitrate);

                if (bandwidthEstimate != null) {
                  // Reset all estimations to zero
                  // Note: this is weird to do this type of "global" side effect
                  // (for this class) in an observable, not too comfortable with
                  // that.
                  this.resetEstimate();
                  nextBitrate = Math.min(
                    bandwidthEstimate,
                    bitrate,
                    maxAutoBitrate
                  );
                }
              }
            }
          }

          // if nextBitrate is not yet defined, do the normal estimation
          if (nextBitrate == null) {
            bandwidthEstimate = this.estimator.getEstimate();

            let nextEstimate;
            if (bandwidthEstimate != null) {
              nextEstimate = inStarvationMode ?
                bandwidthEstimate * ABR_STARVATION_FACTOR :
                bandwidthEstimate * ABR_REGULAR_FACTOR;
            } else if (lastEstimatedBitrate != null) {
              nextEstimate = inStarvationMode ?
                lastEstimatedBitrate * ABR_STARVATION_FACTOR :
                lastEstimatedBitrate * ABR_REGULAR_FACTOR;
            } else {
              nextEstimate = initialBitrate;
            }
            nextBitrate = Math.min(nextEstimate, maxAutoBitrate);
          }

          if (clock.speed > 1) {
            nextBitrate /= clock.speed;
          }

          const _representations =
            getFilteredRepresentations(representations, deviceEvents);

          return {
            bitrate: bandwidthEstimate,
            representation: fromBitrateCeil(_representations, nextBitrate) ||
            representations[0],
          };

        }).do(({ bitrate }) => {
          if (bitrate != null) {
            lastEstimatedBitrate = bitrate;
          }
        }).share();
    });
  }

  /**
   * Add a bandwidth estimate by giving:
   *   - the duration of the request, in s
   *   - the size of the request in bytes
   * @param {Number} duration
   * @param {Number} size
   */
  addEstimate(duration, size) {
    if (duration != null && size != null) {
      this.estimator.addSample(duration, size);
    }
  }

  /**
   * Reset all the estimates done until now.
   * Useful when the network situation changed completely.
   */
  resetEstimate() {
    this.estimator.reset();
  }

  /**
   * Add informations about a new pending request.
   * This can be useful if the network bandwidth drastically changes to infer
   * a new bandwidth through this single request.
   * @param {string|Number} id
   * @param {Segment} segment
   */
  addPendingRequest(id, segment) {
    if (__DEV__) {
      assert(!this._currentRequests[id], "request already added");
    }
    this._currentRequests[id] = segment;
    this._currentRequests[id].progress = [];
  }

  /**
   * Add progress informations to a pending request.
   * Progress objects are a key part to calculate the bandwidth from a single
   * request, in the case the user's bandwidth changes drastically while doing
   * it.
   * @param {string|Number} id
   * @param {Object} progress
   */
  addRequestProgress(id, progress) {
    if (__DEV__) {
      assert(this._currentRequests[id] &&
        this._currentRequests[id].progress, "not a valid request");
    }
    this._currentRequests[id].progress.push(progress);
  }

  /**
   * Remove a request previously set as pending through the addPendingRequest
   * method.
   * @param {string|Number} id
   */
  removePendingRequest(id) {
    if (__DEV__) {
      assert(this._currentRequests[id], "can't remove request: id not found");
    }
    delete this._currentRequests[id];
  }

  /**
   * Remove informations about all pending requests.
   */
  resetRequests() {
    this._currentRequests = {};
  }

  /**
   * TODO Not really needed for now
   */
  dispose() {
    this._dispose$.next();
  }
}
<|endoftext|>