<commit_before>var channels = {"channels": [
    {"channel": "All", "feed": "/r/all/.json"}
    ,{"channel": "Videos", "feed": "/r/videos/.json"}
    ,{"channel": "YouTube", "feed": "/domain/youtube.com/.json"}
    ,{"channel": "Funny", "feed": "/r/funny/.json"}
    ,{"channel": "Politics", "feed": "/r/politics/.json"}
    ,{"channel": "WTF", "feed": "/r/wtf/.json"}
    ,{"channel": "Gaming", "feed": "/r/gaming/.json"}
    ,{"channel": "Science", "feed": "/r/science/.json"}
    ,{"channel": "Geek", "feed": "/r/geek/.json"}
    ,{"channel": "AWW", "feed": "/r/aww/.json"}
    ,{"channel": "Music", "feed": "/r/music/.json"}
    ,{"channel": "Listen", "feed": "/r/listentothis/.json"}
    ,{"channel": "Sports", "feed": "/r/sports/.json"}
    ,{"channel": "TED", "feed": "/domain/ted.com/.json"}
  ]
};

var videos = new Array();
var cur_video = 0;
var cur_chan = 0;
var cur_req = null;
var auto = true;

$().ready(function(){
	displayChannels();
	loadChannel("Videos");
        $('#auto').click(function() {
	        auto = ($('#auto').is(':checked')) ? true : false;
	    });
        $('#fill').click(function() {
	        fill = ($('#fill').is(':checked')) ? true : false;
		fillScreen();
            });
	$('#next-button').click(function() {
		loadVideo('next');
	    });
	$('#prev-button').click(function() {
		loadVideo('prev');;
	    });
	//bind arrow keys
	$(document).keydown(function (e) {
		var keyCode = e.keyCode || e.which;
		var arrow = {left: 37, up: 38, right: 39, down: 40 };
		switch (keyCode) {
		    case arrow.left:
		        loadVideo('prev');
			break;
		    case arrow.up:
		        chgChan('up');
			break;
		    case arrow.right:
			loadVideo('next');
			break;
		    case arrow.down:
		        chgChan('down');
			break;
		    case 32:
		        ytTogglePlay();
		        break;
		    case 70:
		        $('#fill').attr('checked', true);
		        fillScreen();
		        break;
		    case 27:
		        if($('#fill').is(':checked')){
			    fillScreen();
		        }
			break;
		}
	});
});

var displayChannels = function displayChannels() {
    var $channel_list = $('#channel-list');
    var list = $(
		 '
'
		 +'
'
		 );
    $channel_list.html(list);
    for(var x in channels.channels){
	$('#channel-list>ul').append('
'+channels.channels[x].channel+'
');
	$('#channel-'+x).bind(
			       'click'
			       ,{channel: channels.channels[x].channel}
			       , function(event) {
				   loadChannel(event.data.channel);
			       });
    }
}

var loadChannel = function loadChannel(channel) {
    var last_req = cur_req;
    if(last_req != null){
	last_req.abort();
    }

    cur_chan = getChan(channel);

    var $video_embed = $('#video-embed');
    var $video_title = $('#video-title');

    $video_title.html('Loading '+channels.channels[cur_chan].feed.slice(0,-5)+' ...');
    $video_embed.addClass('loading');
    $video_embed.empty();

    $('#channel-list>ul>li').removeClass('chan-selected');
    $('#channel-'+getChan(channel)).addClass('chan-selected');

    var feed = getFeedName(channel);
    cur_req = $.jsonp({
	url: "http://www.reddit.com"+feed+"?limit=100&jsonp=callback",
	callback: "callback",
	success: function(data) {
	    videos = new Array(); //clear out stored videos
            for(var x in data.data.children){
                if(!isEmpty(data.data.children[x].data.media_embed)
                         && data.data.children[x].data.media.type != 'soundcloud.com'
                         && data.data.children[x].data.media.type != 'craigslist.org'
                  )
                {
                    videos.push(data.data.children[x].data);
                }
            }
	    cur_video = 0;
	    loadVideo('first');
	},
	error: function() {
	    alert('Could not load feed. Is reddit down?');
	}
    });
}

var loadVideo = function loadVideo(video) {
    var this_video = cur_video;
    if(video == 'next' && cur_video < Object.size(videos)-1){
	cur_video++;
    }else if (cur_video > 0 && video != 'next'){
	cur_video--;
    }
    if(this_video != cur_video || video == 'first') {
	var title = $.unescapifyHTML(videos[cur_video].title);
	var esc_title = String(title).replace(/\"/g,'"');
	var embed = $.unescapifyHTML(videos[cur_video].media_embed.content);
	if(videos[cur_video].media.type == 'youtube.com'){
	    embed = prepYT(embed);
	}else{
	    yt_player = false;
	}
	var permalink = 'http://reddit.com'+$.unescapifyHTML(videos[cur_video].permalink);
	$('#video-title').html(''+title+'');
	$('#video-embed').html(embed);
	fillScreen();
    }
}

var chgChan = function chgChan(up_down) {
    var this_chan = cur_chan;
    if(up_down == 'up' && this_chan > 0){
	cur_chan--;
    }else if(up_down != 'up' && this_chan < channels.channels.length-1){
	cur_chan++;
    }
    if(this_chan != cur_chan){
	loadChannel(channels.channels[cur_chan].channel);
    }
}

var getFeedName = function getFeedName(channel) {
    for(var x in channels.channels){
	if(channels.channels[x].channel == channel){
	    return channels.channels[x].feed;
	}
    }
}

var getChan = function getChan(channel) {
    for(var x in channels.channels){
        if(channels.channels[x].channel == channel){
            return x;
        }
    }
}

var prepYT = function prepYT(embed) {
    var embed = embed;
    var js_str = '&enablejsapi=1';
    var split = embed.indexOf('?fs=1')+5;
    embed = embed.substr(0,split)+js_str+embed.substr(split);
    split = embed.indexOf('?fs=1" type="')+5;
    embed = embed.substr(0,split)+js_str+embed.substr(split);
    split = embed.indexOf('embed')+5;
    embed = embed.substr(0,split)+' id="ytplayer" '+embed.substr(split);
    return embed;
}

var fillScreen = function fillScreen() {
    if(yt_player){
	$object = $('#video-embed>object>embed');
	$fill = $('#fill');
	if($object.hasClass('fill-screen')){
	    $object.removeClass('fill-screen');
	    $fill.attr('checked', false);
	}else if($fill.is(':checked')){
	    $object.addClass('fill-screen');
	}
    }
}

function onYouTubePlayerReady(playerId) {
    yt_player = document.getElementById("ytplayer");
    yt_player.addEventListener("onStateChange", "ytAuto");
}

function ytAuto(state) {
    if(auto){
	if(state == 0){
	    loadVideo('next');
	}else if(state == 5){
	    ytTogglePlay();
	}
    }
}

function ytTogglePlay() {
    if (yt_player) {
	//unstarted (-1), ended (0), playing (1), paused (2), buffering (3), video cued (5)
	if(yt_player.getPlayerState() != 1){
	    yt_player.playVideo();
	}else{
	    yt_player.pauseVideo();
	}
    }
}

var isEmpty = function isEmpty(obj) {
    for(var prop in obj) {
	if(obj.hasOwnProperty(prop))
            return false;
    }
    return true;
}

Object.size = function(obj) {
    var size = 0, key;
    for (key in obj) {
	if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};

function htmlEncode(value){
    return $('
').text(value).html();
}

function htmlDecode(value){
    return $('
').html(value).text();
}
<commit_msg>fillscreen keyboard shortcut fix - only toggle checkbox if youtube video<commit_after>var channels = {"channels": [
    {"channel": "All", "feed": "/r/all/.json"}
    ,{"channel": "Videos", "feed": "/r/videos/.json"}
    ,{"channel": "YouTube", "feed": "/domain/youtube.com/.json"}
    ,{"channel": "Funny", "feed": "/r/funny/.json"}
    ,{"channel": "Politics", "feed": "/r/politics/.json"}
    ,{"channel": "WTF", "feed": "/r/wtf/.json"}
    ,{"channel": "Gaming", "feed": "/r/gaming/.json"}
    ,{"channel": "Science", "feed": "/r/science/.json"}
    ,{"channel": "Geek", "feed": "/r/geek/.json"}
    ,{"channel": "AWW", "feed": "/r/aww/.json"}
    ,{"channel": "Music", "feed": "/r/music/.json"}
    ,{"channel": "Listen", "feed": "/r/listentothis/.json"}
    ,{"channel": "Sports", "feed": "/r/sports/.json"}
    ,{"channel": "TED", "feed": "/domain/ted.com/.json"}
  ]
};

var videos = new Array();
var cur_video = 0;
var cur_chan = 0;
var cur_req = null;
var auto = true;

$().ready(function(){
	displayChannels();
	loadChannel("Videos");
        $('#auto').click(function() {
	        auto = ($('#auto').is(':checked')) ? true : false;
	    });
        $('#fill').click(function() {
		fillScreen();
            });
	$('#next-button').click(function() {
		loadVideo('next');
	    });
	$('#prev-button').click(function() {
		loadVideo('prev');;
	    });
	//bind arrow keys
	$(document).keydown(function (e) {
		var keyCode = e.keyCode || e.which;
		var arrow = {left: 37, up: 38, right: 39, down: 40 };
		switch (keyCode) {
		    case arrow.left:
		        loadVideo('prev');
			break;
		    case arrow.up:
		        chgChan('up');
			break;
		    case arrow.right:
			loadVideo('next');
			break;
		    case arrow.down:
		        chgChan('down');
			break;
		    case 32:
		        ytTogglePlay();
		        break;
		    case 70:
		        if(yt_player){
			    $('#fill').attr('checked', true);
		            fillScreen();
		        }
		        break;
		    case 27:
		        if($('#fill').is(':checked')){
			    fillScreen();
		        }
			break;
		}
	});
});

var displayChannels = function displayChannels() {
    var $channel_list = $('#channel-list');
    var list = $(
		 '
'
		 +'
'
		 );
    $channel_list.html(list);
    for(var x in channels.channels){
	$('#channel-list>ul').append('
'+channels.channels[x].channel+'
');
	$('#channel-'+x).bind(
			       'click'
			       ,{channel: channels.channels[x].channel}
			       , function(event) {
				   loadChannel(event.data.channel);
			       });
    }
}

var loadChannel = function loadChannel(channel) {
    var last_req = cur_req;
    if(last_req != null){
	last_req.abort();
    }

    cur_chan = getChan(channel);

    var $video_embed = $('#video-embed');
    var $video_title = $('#video-title');

    $video_title.html('Loading '+channels.channels[cur_chan].feed.slice(0,-5)+' ...');
    $video_embed.addClass('loading');
    $video_embed.empty();

    $('#channel-list>ul>li').removeClass('chan-selected');
    $('#channel-'+getChan(channel)).addClass('chan-selected');

    var feed = getFeedName(channel);
    cur_req = $.jsonp({
	url: "http://www.reddit.com"+feed+"?limit=100&jsonp=callback",
	callback: "callback",
	success: function(data) {
	    videos = new Array(); //clear out stored videos
            for(var x in data.data.children){
                if(!isEmpty(data.data.children[x].data.media_embed)
                         && data.data.children[x].data.media.type != 'soundcloud.com'
                         && data.data.children[x].data.media.type != 'craigslist.org'
                  )
                {
                    videos.push(data.data.children[x].data);
                }
            }
	    cur_video = 0;
	    loadVideo('first');
	},
	error: function() {
	    alert('Could not load feed. Is reddit down?');
	}
    });
}

var loadVideo = function loadVideo(video) {
    var this_video = cur_video;
    if(video == 'next' && cur_video < Object.size(videos)-1){
	cur_video++;
    }else if (cur_video > 0 && video != 'next'){
	cur_video--;
    }
    if(this_video != cur_video || video == 'first') {
	var title = $.unescapifyHTML(videos[cur_video].title);
	var esc_title = String(title).replace(/\"/g,'"');
	var embed = $.unescapifyHTML(videos[cur_video].media_embed.content);
	if(videos[cur_video].media.type == 'youtube.com'){
	    embed = prepYT(embed);
	}else{
	    yt_player = false;
	}
	var permalink = 'http://reddit.com'+$.unescapifyHTML(videos[cur_video].permalink);
	$('#video-title').html(''+title+'');
	$('#video-embed').html(embed);
	fillScreen();
    }
}

var chgChan = function chgChan(up_down) {
    var this_chan = cur_chan;
    if(up_down == 'up' && this_chan > 0){
	cur_chan--;
    }else if(up_down != 'up' && this_chan < channels.channels.length-1){
	cur_chan++;
    }
    if(this_chan != cur_chan){
	loadChannel(channels.channels[cur_chan].channel);
    }
}

var getFeedName = function getFeedName(channel) {
    for(var x in channels.channels){
	if(channels.channels[x].channel == channel){
	    return channels.channels[x].feed;
	}
    }
}

var getChan = function getChan(channel) {
    for(var x in channels.channels){
        if(channels.channels[x].channel == channel){
            return x;
        }
    }
}

var prepYT = function prepYT(embed) {
    var embed = embed;
    var js_str = '&enablejsapi=1';
    var split = embed.indexOf('?fs=1')+5;
    embed = embed.substr(0,split)+js_str+embed.substr(split);
    split = embed.indexOf('?fs=1" type="')+5;
    embed = embed.substr(0,split)+js_str+embed.substr(split);
    split = embed.indexOf('embed')+5;
    embed = embed.substr(0,split)+' id="ytplayer" '+embed.substr(split);
    return embed;
}

var fillScreen = function fillScreen() {
    if(yt_player){
	$object = $('#video-embed>object>embed');
	$fill = $('#fill');
	if($object.hasClass('fill-screen')){
	    $object.removeClass('fill-screen');
	    $fill.attr('checked', false);
	}else if($fill.is(':checked')){
	    $object.addClass('fill-screen');
	    $fill.attr('checked', true);
	}
    }
}

function onYouTubePlayerReady(playerId) {
    yt_player = document.getElementById("ytplayer");
    yt_player.addEventListener("onStateChange", "ytAuto");
}

function ytAuto(state) {
    if(auto){
	if(state == 0){
	    loadVideo('next');
	}else if(state == 5){
	    ytTogglePlay();
	}
    }
}

function ytTogglePlay() {
    if (yt_player) {
	//unstarted (-1), ended (0), playing (1), paused (2), buffering (3), video cued (5)
	if(yt_player.getPlayerState() != 1){
	    yt_player.playVideo();
	}else{
	    yt_player.pauseVideo();
	}
    }
}

var isEmpty = function isEmpty(obj) {
    for(var prop in obj) {
	if(obj.hasOwnProperty(prop))
            return false;
    }
    return true;
}

Object.size = function(obj) {
    var size = 0, key;
    for (key in obj) {
	if (obj.hasOwnProperty(key)) size++;
    }
    return size;
};

function htmlEncode(value){
    return $('
').text(value).html();
}

function htmlDecode(value){
    return $('
').html(value).text();
}
<|endoftext|>


<commit_before>/**
 * Functions to build/cache match object
 * */
const constants = require('dotaconstants');
const { promisify } = require('util');
const config = require('../config');
const queries = require('./queries');
const compute = require('../util/compute');
const utility = require('../util/utility');
const cassandra = require('../store/cassandra');
const redis = require('../store/redis');
const db = require('../store/db');

const { computeMatchData } = compute;
const { deserialize, buildReplayUrl, isContributor } = utility;
const getRedisAsync = promisify(redis.get).bind(redis);

async function getMatchData(matchId) {
  const result = await cassandra.execute('SELECT * FROM matches where match_id = ?', [Number(matchId)], {
    prepare: true,
    fetchSize: 1,
    autoPage: true,
  });
  const deserializedResult = result.rows.map(m => deserialize(m));
  return Promise.resolve(deserializedResult[0]);
}

async function getPlayerMatchData(matchId) {
  const result = await cassandra.execute('SELECT * FROM player_matches where match_id = ?', [Number(matchId)], {
    prepare: true,
    fetchSize: 24,
    autoPage: true,
  });
  const deserializedResult = result.rows.map(m => deserialize(m));
  return Promise.all(deserializedResult.map(r => db.raw(`
        SELECT personaname, name, last_login
        FROM players
        LEFT JOIN notable_players
        ON players.account_id = notable_players.account_id
        WHERE players.account_id = ?
      `, [r.account_id])
    .then(names => ({ ...r, ...names.rows[0] }))));
}

async function extendPlayerData(player, match) {
  const p = {
    ...player,
    radiant_win: match.radiant_win,
    start_time: match.start_time,
    duration: match.duration,
    cluster: match.cluster,
    lobby_type: match.lobby_type,
    game_mode: match.game_mode,
    is_contributor: isContributor(player.account_id),
  };
  computeMatchData(p);
  const row = await db.first().from('rank_tier').where({ account_id: p.account_id || null });
  p.rank_tier = row ? row.rating : null;
  return Promise.resolve(p);
}

async function prodataInfo(matchId) {
  const result = await db.first(['radiant_team_id', 'dire_team_id', 'leagueid'])
    .from('matches')
    .where({
      match_id: matchId,
    });
  if (!result) {
    return Promise.resolve({});
  }
  const leaguePromise = db.first().from('leagues').where({
    leagueid: result.leagueid,
  });
  const radiantTeamPromise = db.first().from('teams').where({
    team_id: result.radiant_team_id,
  });
  const direTeamPromise = db.first().from('teams').where({
    team_id: result.dire_team_id,
  });
  const [league, radiantTeam, direTeam] = await Promise.all([leaguePromise, radiantTeamPromise, direTeamPromise]);
  return Promise.resolve({
    league,
    radiant_team: radiantTeam,
    dire_team: direTeam,
  });
}

async function getMatch(matchId) {
  if (!matchId || Number.isNaN(Number(matchId)) || Number(matchId) <= 0) {
    return Promise.resolve();
  }
  const match = await getMatchData(matchId);
  if (!match) {
    return Promise.resolve();
  }
  let playersMatchData = [];
  try {
    playersMatchData = await getPlayerMatchData(matchId);
  } catch (e) {
    if (e.message.startsWith('Server failure during read query')) {
      // Delete and request new
      await cassandra.execute('DELETE FROM player_matches where match_id = ?', [Number(matchId)]);
      const match = {
        match_id: Number(matchId),
      };
      await new Promise((resolve, reject) => {
      utility.getData(utility.generateJob('api_details', match).url, (err, body) => {
        if (err) {
          console.error(err);
          return reject();
        }
        // match details response
        const match = body.result;
        return queries.insertMatch(match, {
          type: 'api',
          attempts: 1,
          priority: 1,
          forceParse: false,
        }, () => {
          // Count for logging
          utility.redisCount(redis, 'cassandra_repair');
          resolve();
        });
      });
    });
    }
  }
  const playersPromise = Promise.all(playersMatchData.map(p => extendPlayerData(p, match)));
  const gcdataPromise = db.first().from('match_gcdata').where({
    match_id: matchId,
  });
  const cosmeticsPromise = Promise.all(Object.keys(match.cosmetics || {}).map(itemId => db.first().from('cosmetics').where({
    item_id: itemId,
  })));
  const prodataPromise = prodataInfo(matchId);

  const [players, gcdata, prodata, cosmetics] = await Promise.all([playersPromise, gcdataPromise, prodataPromise, cosmeticsPromise]);

  let matchResult = {
    ...match,
    ...gcdata,
    ...prodata,
    players,
  };

  if (cosmetics) {
    const playersWithCosmetics = matchResult.players.map((p) => {
      const hero = constants.heroes[p.hero_id] || {};
      const playerCosmetics = cosmetics.filter(Boolean).filter(c => match.cosmetics[c.item_id] === p.player_slot
        && (!c.used_by_heroes || c.used_by_heroes === hero.name));
      return {
        ...p,
        cosmetics: playerCosmetics,
      };
    });
    matchResult = {
      ...matchResult,
      players: playersWithCosmetics,
    };
  }
  computeMatchData(matchResult);
  if (matchResult.replay_salt) {
    matchResult.replay_url = buildReplayUrl(matchResult.match_id, matchResult.cluster, matchResult.replay_salt);
  }
  const matchWithBenchmarks = await queries.getMatchBenchmarksPromisified(matchResult);
  return Promise.resolve(matchWithBenchmarks);
}

async function buildMatch(matchId) {
  const key = `match:${matchId}`;
  const reply = await getRedisAsync(key);
  if (reply) {
    return Promise.resolve(JSON.parse(reply));
  }
  const match = await getMatch(matchId);
  if (!match) {
    return Promise.resolve();
  }
  if (match.version && config.ENABLE_MATCH_CACHE) {
    await redis.setex(key, config.MATCH_CACHE_SECONDS, JSON.stringify(match));
  }
  return Promise.resolve(match);
}

module.exports = buildMatch;
<commit_msg>rethrow exceptions if not repairable<commit_after>/**
 * Functions to build/cache match object
 * */
const constants = require('dotaconstants');
const { promisify } = require('util');
const config = require('../config');
const queries = require('./queries');
const compute = require('../util/compute');
const utility = require('../util/utility');
const cassandra = require('../store/cassandra');
const redis = require('../store/redis');
const db = require('../store/db');

const { computeMatchData } = compute;
const { deserialize, buildReplayUrl, isContributor } = utility;
const getRedisAsync = promisify(redis.get).bind(redis);

async function getMatchData(matchId) {
  const result = await cassandra.execute('SELECT * FROM matches where match_id = ?', [Number(matchId)], {
    prepare: true,
    fetchSize: 1,
    autoPage: true,
  });
  const deserializedResult = result.rows.map(m => deserialize(m));
  return Promise.resolve(deserializedResult[0]);
}

async function getPlayerMatchData(matchId) {
  const result = await cassandra.execute('SELECT * FROM player_matches where match_id = ?', [Number(matchId)], {
    prepare: true,
    fetchSize: 24,
    autoPage: true,
  });
  const deserializedResult = result.rows.map(m => deserialize(m));
  return Promise.all(deserializedResult.map(r => db.raw(`
        SELECT personaname, name, last_login
        FROM players
        LEFT JOIN notable_players
        ON players.account_id = notable_players.account_id
        WHERE players.account_id = ?
      `, [r.account_id])
    .then(names => ({ ...r, ...names.rows[0] }))));
}

async function extendPlayerData(player, match) {
  const p = {
    ...player,
    radiant_win: match.radiant_win,
    start_time: match.start_time,
    duration: match.duration,
    cluster: match.cluster,
    lobby_type: match.lobby_type,
    game_mode: match.game_mode,
    is_contributor: isContributor(player.account_id),
  };
  computeMatchData(p);
  const row = await db.first().from('rank_tier').where({ account_id: p.account_id || null });
  p.rank_tier = row ? row.rating : null;
  return Promise.resolve(p);
}

async function prodataInfo(matchId) {
  const result = await db.first(['radiant_team_id', 'dire_team_id', 'leagueid'])
    .from('matches')
    .where({
      match_id: matchId,
    });
  if (!result) {
    return Promise.resolve({});
  }
  const leaguePromise = db.first().from('leagues').where({
    leagueid: result.leagueid,
  });
  const radiantTeamPromise = db.first().from('teams').where({
    team_id: result.radiant_team_id,
  });
  const direTeamPromise = db.first().from('teams').where({
    team_id: result.dire_team_id,
  });
  const [league, radiantTeam, direTeam] = await Promise.all([leaguePromise, radiantTeamPromise, direTeamPromise]);
  return Promise.resolve({
    league,
    radiant_team: radiantTeam,
    dire_team: direTeam,
  });
}

async function getMatch(matchId) {
  if (!matchId || Number.isNaN(Number(matchId)) || Number(matchId) <= 0) {
    return Promise.resolve();
  }
  const match = await getMatchData(matchId);
  if (!match) {
    return Promise.resolve();
  }
  let playersMatchData = [];
  try {
    playersMatchData = await getPlayerMatchData(matchId);
  } catch (e) {
    if (e.message.startsWith('Server failure during read query')) {
      // Delete and request new
      await cassandra.execute('DELETE FROM player_matches where match_id = ?', [Number(matchId)]);
      const match = {
        match_id: Number(matchId),
      };
      await new Promise((resolve, reject) => {
      utility.getData(utility.generateJob('api_details', match).url, (err, body) => {
        if (err) {
          console.error(err);
          return reject();
        }
        // match details response
        const match = body.result;
        return queries.insertMatch(match, {
          type: 'api',
          attempts: 1,
          priority: 1,
          forceParse: false,
        }, () => {
          // Count for logging
          utility.redisCount(redis, 'cassandra_repair');
          resolve();
        });
      });
    });
    playersMatchData = await getPlayerMatchData(matchId);
    } else {
      throw e;
    }
  }
  const playersPromise = Promise.all(playersMatchData.map(p => extendPlayerData(p, match)));
  const gcdataPromise = db.first().from('match_gcdata').where({
    match_id: matchId,
  });
  const cosmeticsPromise = Promise.all(Object.keys(match.cosmetics || {}).map(itemId => db.first().from('cosmetics').where({
    item_id: itemId,
  })));
  const prodataPromise = prodataInfo(matchId);

  const [players, gcdata, prodata, cosmetics] = await Promise.all([playersPromise, gcdataPromise, prodataPromise, cosmeticsPromise]);

  let matchResult = {
    ...match,
    ...gcdata,
    ...prodata,
    players,
  };

  if (cosmetics) {
    const playersWithCosmetics = matchResult.players.map((p) => {
      const hero = constants.heroes[p.hero_id] || {};
      const playerCosmetics = cosmetics.filter(Boolean).filter(c => match.cosmetics[c.item_id] === p.player_slot
        && (!c.used_by_heroes || c.used_by_heroes === hero.name));
      return {
        ...p,
        cosmetics: playerCosmetics,
      };
    });
    matchResult = {
      ...matchResult,
      players: playersWithCosmetics,
    };
  }
  computeMatchData(matchResult);
  if (matchResult.replay_salt) {
    matchResult.replay_url = buildReplayUrl(matchResult.match_id, matchResult.cluster, matchResult.replay_salt);
  }
  const matchWithBenchmarks = await queries.getMatchBenchmarksPromisified(matchResult);
  return Promise.resolve(matchWithBenchmarks);
}

async function buildMatch(matchId) {
  const key = `match:${matchId}`;
  const reply = await getRedisAsync(key);
  if (reply) {
    return Promise.resolve(JSON.parse(reply));
  }
  const match = await getMatch(matchId);
  if (!match) {
    return Promise.resolve();
  }
  if (match.version && config.ENABLE_MATCH_CACHE) {
    await redis.setex(key, config.MATCH_CACHE_SECONDS, JSON.stringify(match));
  }
  return Promise.resolve(match);
}

module.exports = buildMatch;
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */
FOAModel({
  name: 'AbstractChoiceView',

  extendsModel: 'View',

  properties: [
    {
      name: 'data',
      help: 'The value of the current choice (ie. [value, label] -> value).',
      postSet: function(_, d) {
        for ( var i = 0 ; i < this.choices.length ; i++ ) {
          if ( this.choices[i][0] === d ) {
            if ( this.index !== i ) this.index = i;
            return;
          }
        }
      }
    },
    {
      name: 'choice',
      help: 'The current choice (ie. [value, label]).',
      getter: function() {
        var value = this.data;
        for ( var i = 0 ; i < this.choices.length ; i++ ) {
          var choice = this.choices[i];
          if ( value === choice[0] ) return choice;
        }
        return undefined;
      },
      setter: function(choice) {
        var oldValue = this.choice;
        this.data = choice[0];
        this.propertyChange('choice', oldValue, this.choice);
      }
    },
    {
      name:  'choices',
      type:  'Array[StringField]',
      help: 'Array of [value, label] choices.  Simple String values will be upgraded to [value, value].',
      defaultValue: [],
      preSet: function(_, a) {
        a = a.clone();
        // Upgrade single values to [value, value]
        for ( var i = 0 ; i < a.length ; i++ )
          if ( ! Array.isArray(a[i]) )
            a[i] = [a[i], a[i]];
        return a;
      },
      postSet: function(_, newValue) {
        var value = this.data

        // Update current choice when choices update
        for ( var i = 0 ; i < newValue.length ; i++ ) {
          var choice = newValue[i];

          if ( value === choice[0] ) {
            this.choice = choice;
            break;
          }
        }

        if ( i === newValue.length ) this.data = newValue.length ? newValue[0][0] : undefined;

        if ( this.$ ) this.updateHTML();
      }
    },
    {
      name: 'index',
      help: 'The index of the current choice.',
      postSet: function(_, i) {
        if ( this.data !== this.choices[i][0] ) this.data = this.choices[i][0];
      }
    },
    {
      model_: 'FunctionProperty',
      name: 'objToChoice',
      help: 'A Function which adapts an object from the DAO to a [key, value, ...] choice.'
    },
    {
      name: 'dao',
      postSet: function(oldDAO, dao) {
        if ( oldDAO ) oldDAO.unlisten(this.onDAOUpdate);
        dao.listen(this.onDAOUpdate);
        this.onDAOUpdate();
      }
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isMerged: 100,
      code: function() {
        this.dao.select(MAP(this.objToChoice))(function(map) {
          this.choices = map.arg2;
        }.bind(this));
      }
    }
  ],

  methods: {
    findChoiceIC: function(name) {
      name = name.toLowerCase();
      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        if ( this.choices[i][1].toLowerCase() == name )
          return this.choices[i];
      }
    }
  }
});

FOAModel({
  name:  'ChoiceListView',

  extendsModel: 'AbstractChoiceView',

  properties: [
    {
      name: 'orientation',
      defaultValue: 'horizontal',
      view: {
        model_: 'ChoiceView',
        choices: [
          [ 'horizontal', 'Horizontal' ],
          [ 'vertical',   'Vertical'   ]
        ]
      },
      postSet: function(old, nu) {
        if ( this.$ ) {
          DOM.setClass(this.$, old, false);
          DOM.setClass(this.$, nu);
        }
      }
    },
    {
      name: 'className',
      defaultValueFn: function() { return 'foamChoiceListView ' + this.orientation; }
    },
    {
      name: 'tagName',
      defaultValue: 'ul'
    }
  ],

  methods: {
    choiceToHTML: function(id, choice) {
      return '
' + choice[1] + '
';
    },
    toInnerHTML: function() {
      var out = "";
      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        var choice = this.choices[i];
        var id     = this.nextID();

        this.on(
          'click',
          function(choice) {
            this.choice = choice;
          }.bind(this, choice),
          id);

        this.setClass(
          'selected',
          function(choice) { return this.choice == choice; }.bind(this, choice),
          id);

        out += this.choiceToHTML(id, choice);
      }
      return out;
    }
  }
});

FOAModel({
  name:  'ChoiceView',

  extendsModel: 'AbstractChoiceView',

  /*
   * <select size="">
   *    <choice value="" selected></choice>
   * </select>
   */
  properties: [
    {
      name:  'name',
      type:  'String',
      defaultValue: 'field'
    },
    {
      name:  'helpText',
      type:  'String',
      defaultValue: undefined
    },
    {
      name:  'size',
      type:  'int',
      defaultValue: 1
    }
  ],

  methods: {
    toHTML: function() {
      return '<select id="' + this.id + '" name="' + this.name + '" size=' + this.size + '/></select>';
    },

    updateHTML: function() {
      var out = [];

      if ( this.helpText ) {
        out.push('<option disabled="disabled">');
        out.push(this.helpText);
        out.push('</option>');
      }

      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        var choice = this.choices[i];
        var id     = this.nextID();

        try {
          this.on('click', this.onClick, id);
          this.on('mouseover', this.onMouseOver, id);
          this.on('mouseout', this.onMouseOut, id);
        } catch (x) {
          // Fails on iPad, which is okay, because this feature doesn't make
          // sense on the iPad anyway.
        }

        out.push('\t<option id="' + id + '"');

        if ( choice[0] === this.data ) out.push(' selected');
        out.push(' value="');
        out.push(i + '">');
        out.push(choice[1].toString());
        out.push('</option>');
      }

      this.$.innerHTML = out.join('');
      View.getPrototype().initHTML.call(this);
    },

    initHTML: function() {
      var e = this.$;

      this.updateHTML();
      this.domValue = DomValue.create(e);
      Events.link(this.index$, this.domValue);
    }
  },

  listeners: [
    {
      name: 'onMouseOver',
      code: function(e) {
        if ( this.timer_ ) window.clearTimeout(this.timer_);
        this.prev = ( this.prev === undefined ) ? this.data : this.prev;
        this.index = e.target.value;
      }
    },
    {
      name: 'onMouseOut',
      code: function(e) {
        if ( this.timer_ ) window.clearTimeout(this.timer_);
        this.timer_ = window.setTimeout(function() {
          this.data = this.prev || '';
          this.prev = undefined;
        }.bind(this), 1);
      }
    },
    {
      name: 'onClick',
      code: function(e) {
        this.data = this.prev = this.choices[e.target.value][0];
      }
    }
  ]
});

FOAModel({
  name:  'RadioBoxView',

  extendsModel: 'ChoiceView',

  methods: {
    toHTML: function() {
      return '';
    },

    updateHTML: function() {
      var out = '';
      var self = this;
      this.choices.forEach(function(choice) {
        var value  = choice[0];
        var label  = choice[1];
        var id     = self.nextID();

        out += label + ':<input type="radio" name="';
        out += self.id;
        out += '" value="';
        out += value;
        out += '" ';
        out += 'id="' + id + '"';
        if ( self.data === value ) out += ' checked';
        out += '> ';

        self.on('click', function() { self.data = value; }, id)
        self.data$.addListener(function() { $(id).checked = ( self.data == value ); });
      });

      this.$.innerHTML = out;
      View.getPrototype().initHTML.call(this);
    },

    initHTML: function() {
      Events.dynamic(function() { this.choices; }.bind(this), this.updateHTML.bind(this));
    }
  }
});

FOAModel({
  name:  'PopupChoiceView',

  extendsModel: 'AbstractChoiceView',

  properties: [
    {
      name: 'label'
    },
    {
      name: 'iconUrl'
    },
    {
      name: 'tagName',
      defaultValue: 'button'
    },
    {
      name: 'className',
      defaultValue: 'popupChoiceView'
    },
    {
      model_: 'BooleanProperty',
      name: 'showValue'
    }
  ],

  listeners: [
    {
      name: 'popup',
      code: function(e) {
        var view = this.X.ChoiceListView.create({
          className: 'popupChoiceList',
          data: this.data,
          choices: this.choices
        });

        // I don't know why the 'animate' is required, but it sometimes
        // doesn't remove the view without it.
        view.data$.addListener(EventService.animate(function() {
          this.data = view.data;
          if ( view.$ ) view.$.remove();
        }.bind(this)));

        var pos = findPageXY(this.$.querySelector('.action'));
        var e = this.X.document.body.insertAdjacentHTML('beforeend', view.toHTML());
        var s = this.X.window.getComputedStyle(view.$);
        var parentNode = view.$.parentNode;

        view.$.style.top = pos[1]-2;
        view.$.style.left = pos[0]-toNum(s.width)+30;
        view.$.style.maxHeight = Math.max(200, this.X.window.innerHeight-pos[1]-10);
        view.initHTML();
        view.$.addEventListener('click', function() { if ( view.$ ) view.$.remove(); });
        parentNode.addEventListener('mousemove', function(evt) {
          if ( ! view.$ ) {
            parentNode.removeEventListener('mousemove', arguments.callee);
          } else if ( ! view.$.contains(evt.target) ) {
            parentNode.removeEventListener('mousemove', arguments.callee);
            view.$.remove();
          }
        });
      }
    }
  ],

  methods: {
    toInnerHTML: function() {
      var out = '';

      if ( this.showValue ) {
        var id = this.nextID();
        out += '' + (this.choice[1] || '') + '';
        this.data$.addListener(function() { this.X.$(id).innerHTML = this.choice[1]; }.bind(this));
      }

      out += '';
      if ( this.iconUrl ) {
        out += '';
      }

      if ( this.label ) {
        out += this.label;
      }
      out += '';

      return out;
    },

    initHTML: function() {
      this.$.addEventListener('click', this.popup);
    }
  }
});
<commit_msg>Added 'autoSetData' property to ChoiceView.<commit_after>/**
 * @license
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */
FOAModel({
  name: 'AbstractChoiceView',

  extendsModel: 'View',

  properties: [
    {
      model_: 'BooleanProperty',
      name: 'autoSetData',
      help: 'If true, this.data is set when choices update and the current data is not one of the choices.',
      defaultValue: true
    },
    {
      name: 'data',
      help: 'The value of the current choice (ie. [value, label] -> value).',
      postSet: function(_, d) {
        for ( var i = 0 ; i < this.choices.length ; i++ ) {
          if ( this.choices[i][0] === d ) {
            if ( this.index !== i ) this.index = i;
            return;
          }
        }
      }
    },
    {
      name: 'choice',
      help: 'The current choice (ie. [value, label]).',
      getter: function() {
        var value = this.data;
        for ( var i = 0 ; i < this.choices.length ; i++ ) {
          var choice = this.choices[i];
          if ( value === choice[0] ) return choice;
        }
        return undefined;
      },
      setter: function(choice) {
        var oldValue = this.choice;
        this.data = choice[0];
        this.propertyChange('choice', oldValue, this.choice);
      }
    },
    {
      name:  'choices',
      type:  'Array[StringField]',
      help: 'Array of [value, label] choices.  Simple String values will be upgraded to [value, value].',
      defaultValue: [],
      preSet: function(_, a) {
        a = a.clone();
        // Upgrade single values to [value, value]
        for ( var i = 0 ; i < a.length ; i++ )
          if ( ! Array.isArray(a[i]) )
            a[i] = [a[i], a[i]];
        return a;
      },
      postSet: function(_, newValue) {
        var value = this.data

        // Update current choice when choices update
        for ( var i = 0 ; i < newValue.length ; i++ ) {
          var choice = newValue[i];

          if ( value === choice[0] ) {
            this.choice = choice;
            break;
          }
        }

        if ( this.autoSetData && i === newValue.length ) this.data = newValue.length ? newValue[0][0] : undefined;

        if ( this.$ ) this.updateHTML();
      }
    },
    {
      name: 'index',
      help: 'The index of the current choice.',
      postSet: function(_, i) {
        if ( this.data !== this.choices[i][0] ) this.data = this.choices[i][0];
      }
    },
    {
      model_: 'FunctionProperty',
      name: 'objToChoice',
      help: 'A Function which adapts an object from the DAO to a [key, value, ...] choice.'
    },
    {
      name: 'dao',
      postSet: function(oldDAO, dao) {
        if ( oldDAO ) oldDAO.unlisten(this.onDAOUpdate);
        dao.listen(this.onDAOUpdate);
        this.onDAOUpdate();
      }
    }
  ],

  listeners: [
    {
      name: 'onDAOUpdate',
      isMerged: 100,
      code: function() {
        this.dao.select(MAP(this.objToChoice))(function(map) {
          this.choices = map.arg2;
        }.bind(this));
      }
    }
  ],

  methods: {
    findChoiceIC: function(name) {
      name = name.toLowerCase();
      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        if ( this.choices[i][1].toLowerCase() == name )
          return this.choices[i];
      }
    }
  }
});

FOAModel({
  name:  'ChoiceListView',

  extendsModel: 'AbstractChoiceView',

  properties: [
    {
      name: 'orientation',
      defaultValue: 'horizontal',
      view: {
        model_: 'ChoiceView',
        choices: [
          [ 'horizontal', 'Horizontal' ],
          [ 'vertical',   'Vertical'   ]
        ]
      },
      postSet: function(old, nu) {
        if ( this.$ ) {
          DOM.setClass(this.$, old, false);
          DOM.setClass(this.$, nu);
        }
      }
    },
    {
      name: 'className',
      defaultValueFn: function() { return 'foamChoiceListView ' + this.orientation; }
    },
    {
      name: 'tagName',
      defaultValue: 'ul'
    }
  ],

  methods: {
    choiceToHTML: function(id, choice) {
      return '
' + choice[1] + '
';
    },
    toInnerHTML: function() {
      var out = "";
      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        var choice = this.choices[i];
        var id     = this.nextID();

        this.on(
          'click',
          function(choice) {
            this.choice = choice;
          }.bind(this, choice),
          id);

        this.setClass(
          'selected',
          function(choice) { return this.choice === choice; }.bind(this, choice),
          id);

        out += this.choiceToHTML(id, choice);
      }
      return out;
    }
  }
});

FOAModel({
  name:  'ChoiceView',

  extendsModel: 'AbstractChoiceView',

  /*
   * <select size="">
   *    <choice value="" selected></choice>
   * </select>
   */
  properties: [
    {
      name:  'name',
      type:  'String',
      defaultValue: 'field'
    },
    {
      name:  'helpText',
      type:  'String',
      defaultValue: undefined
    },
    {
      name:  'size',
      type:  'int',
      defaultValue: 1
    }
  ],

  methods: {
    toHTML: function() {
      return '<select id="' + this.id + '" name="' + this.name + '" size=' + this.size + '/></select>';
    },

    updateHTML: function() {
      var out = [];

      if ( this.helpText ) {
        out.push('<option disabled="disabled">');
        out.push(this.helpText);
        out.push('</option>');
      }

      for ( var i = 0 ; i < this.choices.length ; i++ ) {
        var choice = this.choices[i];
        var id     = this.nextID();

        try {
          this.on('click', this.onClick, id);
          this.on('mouseover', this.onMouseOver, id);
          this.on('mouseout', this.onMouseOut, id);
        } catch (x) {
          // Fails on iPad, which is okay, because this feature doesn't make
          // sense on the iPad anyway.
        }

        out.push('\t<option id="' + id + '"');

        if ( choice[0] === this.data ) out.push(' selected');
        out.push(' value="');
        out.push(i + '">');
        out.push(choice[1].toString());
        out.push('</option>');
      }

      this.$.innerHTML = out.join('');
      View.getPrototype().initHTML.call(this);
    },

    initHTML: function() {
      var e = this.$;

      this.updateHTML();
      this.domValue = DomValue.create(e);
      Events.link(this.index$, this.domValue);
    }
  },

  listeners: [
    {
      name: 'onMouseOver',
      code: function(e) {
        if ( this.timer_ ) window.clearTimeout(this.timer_);
        this.prev = ( this.prev === undefined ) ? this.data : this.prev;
        this.index = e.target.value;
      }
    },
    {
      name: 'onMouseOut',
      code: function(e) {
        if ( this.timer_ ) window.clearTimeout(this.timer_);
        this.timer_ = window.setTimeout(function() {
          this.data = this.prev || '';
          this.prev = undefined;
        }.bind(this), 1);
      }
    },
    {
      name: 'onClick',
      code: function(e) {
        this.data = this.prev = this.choices[e.target.value][0];
      }
    }
  ]
});

FOAModel({
  name:  'RadioBoxView',

  extendsModel: 'ChoiceView',

  methods: {
    toHTML: function() {
      return '';
    },

    updateHTML: function() {
      var out = '';
      var self = this;
      this.choices.forEach(function(choice) {
        var value  = choice[0];
        var label  = choice[1];
        var id     = self.nextID();

        out += label + ':<input type="radio" name="';
        out += self.id;
        out += '" value="';
        out += value;
        out += '" ';
        out += 'id="' + id + '"';
        if ( self.data === value ) out += ' checked';
        out += '> ';

        self.on('click', function() { self.data = value; }, id)
        self.data$.addListener(function() { $(id).checked = ( self.data == value ); });
      });

      this.$.innerHTML = out;
      View.getPrototype().initHTML.call(this);
    },

    initHTML: function() {
      Events.dynamic(function() { this.choices; }.bind(this), this.updateHTML.bind(this));
    }
  }
});

FOAModel({
  name:  'PopupChoiceView',

  extendsModel: 'AbstractChoiceView',

  properties: [
    {
      name: 'label'
    },
    {
      name: 'iconUrl'
    },
    {
      name: 'tagName',
      defaultValue: 'button'
    },
    {
      name: 'className',
      defaultValue: 'popupChoiceView'
    },
    {
      model_: 'BooleanProperty',
      name: 'showValue'
    }
  ],

  listeners: [
    {
      name: 'popup',
      code: function(e) {
        var view = this.X.ChoiceListView.create({
          className: 'popupChoiceList',
          data: this.data,
          choices: this.choices,
          autoSetData: this.autoSetData
        });

        // I don't know why the 'animate' is required, but it sometimes
        // doesn't remove the view without it.
        view.data$.addListener(EventService.animate(function() {
          this.data = view.data;
          if ( view.$ ) view.$.remove();
        }.bind(this)));

        var pos = findPageXY(this.$.querySelector('.action'));
        var e = this.X.document.body.insertAdjacentHTML('beforeend', view.toHTML());
        var s = this.X.window.getComputedStyle(view.$);
        var parentNode = view.$.parentNode;

        view.$.style.top = pos[1]-2;
        view.$.style.left = pos[0]-toNum(s.width)+30;
        view.$.style.maxHeight = Math.max(200, this.X.window.innerHeight-pos[1]-10);
        view.initHTML();
        view.$.addEventListener('click', function() { if ( view.$ ) view.$.remove(); });
        parentNode.addEventListener('mousemove', function(evt) {
          if ( ! view.$ ) {
            parentNode.removeEventListener('mousemove', arguments.callee);
          } else if ( ! view.$.contains(evt.target) ) {
            parentNode.removeEventListener('mousemove', arguments.callee);
            view.$.remove();
          }
        });
      }
    }
  ],

  methods: {
    toInnerHTML: function() {
      var out = '';

      if ( this.showValue ) {
        var id = this.nextID();
        out += '' + (this.choice[1] || '') + '';
        this.data$.addListener(function() { this.X.$(id).innerHTML = this.choice[1]; }.bind(this));
      }

      out += '';
      if ( this.iconUrl ) {
        out += '';
      }

      if ( this.label ) {
        out += this.label;
      }
      out += '';

      return out;
    },

    initHTML: function() {
      this.$.addEventListener('click', this.popup);
    }
  }
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */

function IN_BROWSER() { return typeof vm == "undefined" || ! vm.runInThisContext; }
function IN_NODEJS() { return ! IN_BROWSER(); }

var files = [
//  ['ServiceWorker', function() { return window.navigator && navigator.serviceWorker; }],
  [ 'firefox',  function() { return window.navigator && navigator.userAgent.indexOf('Firefox') != -1; }],
  [ 'funcName', function() { return ! Number.name; }],
  [ 'safari',   function() { return window.navigator && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1; }],
  [ 'node',     IN_NODEJS ],
  [ 'i18n',     IN_BROWSER ],
  'stdlib',
  ['WeakMap',  function() { return ! this['WeakMap']; }],
  'writer',
  'socket',
  'async',
  'parse',
  'event',
  'JSONUtil',
  'XMLUtil',
  'context',
  'JSONParser',
  'TemplateUtil',
  'FOAM',
  // To use FO, uncomment the next line
  // and comment out all lines from FObject to mm5Misc
  // inclusively
//  'experimental/fo',
//  'experimental/protobuffeatures',
  'FObject',
  'BootstrapModel',
  'mm1Model',
  'mm2Property',
  'mm3Types',
  'mm4Method',
  'mm5Misc',
  [ '../js/foam/core/bootstrap/ModelFileDAO', IN_BROWSER ],
  [ '../js/node/dao/ModelFileDAO', IN_NODEJS ],
  [ '../js/node/dao/JSModelFileDAO', IN_NODEJS ],
  '../js/foam/ui/Window',
  'value',
  '../js/foam/patterns/ChildTreeTrait', // used in view, included here for backward compat of old apps
  'view',
  'view2',
  'AbstractDAOView',
  'DAOListView',
  'daoView',
  'ChoiceView',
  'DetailView',
  'TableView',
  'cview',
  'RichTextView',
  'listchoiceview',
  'scroll',
  'HTMLParser',
  'mlang',
  'mlang2',
  'QueryParser',
  'search',
  'oam',
  'visitor',
  'messaging',
  'dao',
  'dao2',
  'arrayDAO',
  'diff',
  'index',
  [ 'StackView', IN_BROWSER ],
  [ 'MementoMgr', IN_BROWSER ],
  [ 'DAOController', IN_BROWSER ],
  'models',
  'touch',
  [ 'glang', IN_BROWSER ],
  'oauth'
];
<commit_msg>Remove some IN_BROWSER predicates to allow models to load in nodejs that get used during a build.<commit_after>/**
 * @license
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * 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.
 */

function IN_BROWSER() { return typeof vm == "undefined" || ! vm.runInThisContext; }
function IN_NODEJS() { return ! IN_BROWSER(); }

var files = [
//  ['ServiceWorker', function() { return window.navigator && navigator.serviceWorker; }],
  [ 'firefox',  function() { return window.navigator && navigator.userAgent.indexOf('Firefox') != -1; }],
  [ 'funcName', function() { return ! Number.name; }],
  [ 'safari',   function() { return window.navigator && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1; }],
  [ 'node',     IN_NODEJS ],
  [ 'i18n',     IN_BROWSER ],
  'stdlib',
  ['WeakMap',  function() { return ! this['WeakMap']; }],
  'writer',
  'socket',
  'async',
  'parse',
  'event',
  'JSONUtil',
  'XMLUtil',
  'context',
  'JSONParser',
  'TemplateUtil',
  'FOAM',
  // To use FO, uncomment the next line
  // and comment out all lines from FObject to mm5Misc
  // inclusively
//  'experimental/fo',
//  'experimental/protobuffeatures',
  'FObject',
  'BootstrapModel',
  'mm1Model',
  'mm2Property',
  'mm3Types',
  'mm4Method',
  'mm5Misc',
  [ '../js/foam/core/bootstrap/ModelFileDAO', IN_BROWSER ],
  [ '../js/node/dao/ModelFileDAO', IN_NODEJS ],
  [ '../js/node/dao/JSModelFileDAO', IN_NODEJS ],
  '../js/foam/ui/Window',
  'value',
  '../js/foam/patterns/ChildTreeTrait', // used in view, included here for backward compat of old apps
  'view',
  'view2',
  'AbstractDAOView',
  'DAOListView',
  'daoView',
  'ChoiceView',
  'DetailView',
  'TableView',
  'cview',
  'RichTextView',
  'listchoiceview',
  'scroll',
  'HTMLParser',
  'mlang',
  'mlang2',
  'QueryParser',
  'search',
  'oam',
  'visitor',
  'messaging',
  'dao',
  'dao2',
  'arrayDAO',
  'diff',
  'index',
  'StackView',
  'MementoMgr',
  'DAOController',
  'models',
  'touch',
  [ 'glang', IN_BROWSER ],
  'oauth'
];
<|endoftext|>


<commit_before>
var gd = require('node-gd');
var fs = require('fs');

var fonts = './font/';

exports.clock = function(req, res, next) {
    var img = gd.createTrueColor(800, 100);
    var font = font = 'DigitalDream.ttf';

    img.alphaBlending(1);
	img.setAntiAliased(1);

	var almostblack = img.colorAllocate(1, 1, 1),
	   black = img.colorAllocate(0, 0, 0),
	   white = img.colorAllocate(255, 255, 255),
	   green = img.colorAllocate(0, 200, 0),
	   clear = img.colorAllocateAlpha(255, 255, 255, 0);

	img.fill(0, 0, black);

	var newpix = date.getHours() + date.getDate() * 24 + 2450;
	newpix += date.getMinutes() / 60.0;
	newpix += date.getSeconds() / 3600.0;
	newpix = round(newpix, 2);

    var date = new Date(),
	   countdownh = date.getHours() + date.getDate() * 24;
	countdownh = 669 - countdownh;
	var countdownm = 60 - date.getMinutes();
	if (countdownm < 10) { countdownm = "0" + countdownm; }
	if (countdownh < 10) { countdownh = "0" + countdownh; }

    //img.stringFTBBox  (color, font, size, angle, x, y, str);
    //img.stringFT      (color, font, size, angle, x, y, str);
	img.stringFT(green, font, 20, 0, 20, 62, "Newpix " + newpix);
	img.stringFT(green, font, 20, 0, 350, 62, "Reconnect in: 0:" + countdownh + ":" + countdownm);

    img.savePng('./temp');
    var dat = fs.readFileSync('./temp');

    res.set('Content-Type', 'image/png');
    res.end(dat, 'binary');
    img.destroy();
}<commit_msg>Move date object definition up so both declaration blocks can use it<commit_after>
var gd = require('node-gd');
var fs = require('fs');

var fonts = './font/';

exports.clock = function(req, res, next) {
    var img = gd.createTrueColor(800, 100);
    var font = font = 'DigitalDream.ttf';

    img.alphaBlending(1);
	img.setAntiAliased(1);

	var almostblack = img.colorAllocate(1, 1, 1),
	   black = img.colorAllocate(0, 0, 0),
	   white = img.colorAllocate(255, 255, 255),
	   green = img.colorAllocate(0, 200, 0),
	   clear = img.colorAllocateAlpha(255, 255, 255, 0);

	img.fill(0, 0, black);

    var date = new Date(),
	   newpix = date.getHours() + date.getDate() * 24 + 2450;
	newpix += date.getMinutes() / 60.0;
	newpix += date.getSeconds() / 3600.0;
	newpix = round(newpix, 2);

    var countdownh = date.getHours() + date.getDate() * 24;
	countdownh = 669 - countdownh;
	var countdownm = 60 - date.getMinutes();
	if (countdownm < 10) { countdownm = "0" + countdownm; }
	if (countdownh < 10) { countdownh = "0" + countdownh; }

    //img.stringFTBBox  (color, font, size, angle, x, y, str);
    //img.stringFT      (color, font, size, angle, x, y, str);
	img.stringFT(green, font, 20, 0, 20, 62, "Newpix " + newpix);
	img.stringFT(green, font, 20, 0, 350, 62, "Reconnect in: 0:" + countdownh + ":" + countdownm);

    img.savePng('./temp');
    var dat = fs.readFileSync('./temp');

    res.set('Content-Type', 'image/png');
    res.end(dat, 'binary');
    img.destroy();
}<|endoftext|>


<commit_before>/**
 * @file Contains logic to actually pivot data
*/

/**
 * @description Format data into an array of objects where the headers are the keys
 * @param {!Array|Object} data Array of arrays or an object
 * @returns {Array<Object>} Formatted object
*/
export function fixDataFormat(data, rows = []) {
  if (!Array.isArray(data) || !data.length) return [];

  let formattedData = [];

  if (typeof data[0] === 'object' && !Array.isArray(data[0])) {
    formattedData = data;
  } else {
    formattedData = data.reduce((dataCol, row, i, arr) => {
      if (i !== 0) {
        if (Array.isArray(row)) {
          dataCol.push(row.reduce((acc, curr, index) =>{
            acc[arr[0][index]] = curr;
            return acc;
          }, {}));
        } else {
          dataCol.push({[arr[0]]: row});
        }
      }
      return dataCol;
    }, []);
  }

  // sorting data initially to prevent changes in data ordering
  if (rows.length > 0) {
    return rows.reduceRight((acc, row) => {
      return acc.sort((a, b) => {
        return a[row] > b[row];
      });
    }, formattedData);
  }

  return formattedData.sort((a, b) => {
    return JSON.stringify(a) > JSON.stringify(b);
  });
}

/**
 * @description Groups the data into an object by the provided category
 * @param {!Array<Object>} data Array of objects
 * @param {string} groupBy Category to group by
 * @returns {Object} Each key in the object is one the category groups
*/
export function groupByCategory(data, groupBy) {
  return data.reduce((acc, curr) =>{
    const category = curr[groupBy];

    if (!acc[category]) acc[category] = [];
    acc[category].push(curr);
    return acc;
  }, {});
}

/**
 * @description Performs groupByCategory recursively (nesting objects within objects)
 * @param {!Array<Object>} data Array of objects
 * @param {Array} groups Items to categorize by
 * @param {Object} acc The accumulated results
 * @returns {Object} Deeply nested object
 * where each key is one the category groups
*/
export function groupByCategories(data, groups = [], acc = {}) {
  /**
   * base case - if data is empty
   * or if there are no more groups to group by
   * return result
  */
  if (data.length === 0 || groups.length === 0) return data;

  const groupedData = groupByCategory(data, groups[0]);
  const groupedDataKeys = Object.keys(groupedData);
  const children = Object.values(groupedData);

  for (let i = 0; i < children.length; i++) {
    acc[groupedDataKeys[i]] = groupByCategories(
        children[i], groups.slice(1), acc[groupedDataKeys[i]]);
  }

  return acc;
}

/**
 * @description Builds the column headers and a map to each header
 * @param {!Array<Object>} data Array of objects
 * @param {Array} cols Columns to pivot on
 * @param {string} firstColumn A string to place in the first column header
 * @returns {Object} columnHeaders (array of arrays) and mapToHeader (object)
*/
export function createColumnHeaders(data, cols = [], firstColumn = '') {
  if (cols.length === 0) {
    return {
      columnHeaders: [firstColumn],
      mapToHeader: null,
    };
  }

  const mapToHeader = groupByCategories(data, cols);
  const columnHeaders = [];
  let mapPos = 1;

  (function columnHeaderRecursion(data, pos = 0, headerMap) {
    /** base case - at actual data as opposed to another grouping */
    if (typeof data !== 'object' || Array.isArray(data)) return 1;

    const currKeys = Object.keys(data);
    let sumLength = 0;

    for (let i = 0; i < currKeys.length; i++) {
      const currLength = columnHeaderRecursion(
          data[currKeys[i]], pos + 1, headerMap[currKeys[i]]);

      if (Array.isArray(data[currKeys[i]])) {
        headerMap[currKeys[i]] = mapPos;
        mapPos += 1;
      }

      columnHeaders[pos] = typeof columnHeaders[pos] === 'undefined' ?
        [firstColumn].concat(Array(currLength).fill(currKeys[i])) :
        columnHeaders[pos].concat(Array(currLength).fill(currKeys[i]));

      sumLength += currLength;
    }

    return sumLength;

  }(mapToHeader, 0, mapToHeader));

  return {
    columnHeaders,
    mapToHeader,
  };
}

/**
 * @description Reduces an array of values to a result
 * @param {!Array} arr The array to reduce
 * @param {?requestCallback|string} accCat Callback, category to reduce, or null
 * @param {string} accType Reduce type (count, average, min, max, etc) or initial value
 * @param {*} accValue Initial value - string, number, array, or object
 * @returns {*} Reduced value
 * @todo Move accumulator to its own file since it will continue to grow
*/
export function accumulator(arr, accCat, accType, accValue) {
  if (typeof accCat === 'undefined') accType = 'count';
  else if (typeof accCat === 'function') accValue = accType || 0;

  return arr.reduce((acc, curr, index, array) => {
    if (typeof accCat === 'function') return accCat(acc, curr, index, array);

    switch (accType) {
      case ('average'): {
        acc += Number(curr[accCat]) / arr.length;

        return acc;
      }

      case ('count'): {
        acc += 1;

        return acc;
      }

      case ('min'): {
        if (index === 0) acc = Number(curr[accCat]);
        else if (curr[accCat] < acc) acc = Number(curr[accCat]);

        return acc;
      }

      case ('max'): {
        if (index === 0) acc = Number(curr[accCat]);
        else if (curr[accCat] > acc) acc = Number(curr[accCat]);

        return acc;
      }

      case ('sum'): {
        acc += Number(curr[accCat]);

        return acc;
      }

      default: {
        acc += 1;

        return acc;
      }
    }
  }, accValue || 0);
}

/**
 * @description Check that categories to pivot on actually exist
 * @param {!Object} actualCats Categories that actually exist in the data
 * @param {!Array<string>} selectedCats Categories to pivot selected by user
 * @throws Will throw an error if the category does not exist
*/
export function checkPivotCategories(actualCats, selectedCats) {
  const errMessage = selectedCats.filter((selectedCat) => {
    return !(selectedCat in actualCats);
  });

  if (errMessage.length > 0) {
    throw new Error('Check that these selected pivot categories exist: ' +
      errMessage.join(', '));
  }
}

export function tableCreator(data, rows = [], cols = [], accCatOrCB,
  accTypeOrInitVal, rowHeader) {

  /** if data is empty, return empty array */
  if (data.length === 0) {
    return {
      rawData: [],
      table: [],
    };
  };

  /** if rows/cols are not arrays, return throw an error */
  if (!Array.isArray(rows) || !Array.isArray(cols)) {
    throw new Error('rowsToPivot and colsToPivot must be of type array');
  }

  checkPivotCategories(data[0], rows);
  checkPivotCategories(data[0], cols);

  if (typeof rowHeader === 'undefined') {
    rowHeader = typeof accCatOrCB !== 'function' ?
      `${accTypeOrInitVal} ${accCatOrCB}` :
      'Custom Agg';
  }

  const columnData = createColumnHeaders(data, cols, rowHeader);
  const columnHeaders = Array.isArray(columnData.columnHeaders[0]) ?
    columnData.columnHeaders :
    [columnData.columnHeaders.concat(rowHeader)];
  const mapToHeader = columnData.mapToHeader;
  const headerLength = columnHeaders[0].length;
  const formattedColumnHeaders = columnHeaders.map((value, depth) => {
    return {
      value,
      depth,
      type: 'colHeader',
      row: depth,
    };
  });

  let dataRows = [];
  let rawData = [];
  let prevKey = null;

  function rowRecurse(rowGroups, depth, rowHeaders = []) {
    for (const key in rowGroups) {
      if (Array.isArray(rowGroups[key])) {
        const recursedData = groupByCategories(rowGroups[key], cols);

        prevKey = null;

        (function recurseThroughMap(dataPos, map) {
          if (Array.isArray(dataPos)) {
            if (key === prevKey) {
              const datum = dataRows[dataRows.length - 1].value;

              datum[map] = accumulator(dataPos, accCatOrCB, accTypeOrInitVal);
              dataRows[dataRows.length - 1].value = datum;

              const rawDataDatum = rawData[rawData.length - 1].value;

              rawDataDatum[map] = dataPos;
              rawData[rawData.length - 1].value = rawDataDatum;
            } else {
              prevKey = key;
              const datum = [key].concat(
                Array(map - 1).fill(''),
                accumulator(dataPos, accCatOrCB, accTypeOrInitVal),
                Array(headerLength - (map + 1)).fill('')
              );
              const rawDataDatum = [key].concat(
                Array(map - 1).fill(''),
                [dataPos],
                Array(headerLength - (map + 1)).fill('')
              );

              rawData.push({
                value: rawDataDatum,
                type: 'data',
                depth,
              });
              dataRows.push({
                value: datum,
                type: 'data',
                depth,
                row: dataRows.length + formattedColumnHeaders.length,
              });
            }
          } else {
            for (const innerKey in dataPos) {
              recurseThroughMap(dataPos[innerKey], map[innerKey]);
            }
          }
        })(recursedData, mapToHeader || 1);

      } else {
        const rowHeaderValue = rowHeaders.shift();
        const value = rowHeaderValue ?
            rowHeaderValue.value :
            [key].concat(Array(headerLength - 1).fill(''));

        dataRows.push({
          value,
          depth,
          type: 'rowHeader',
          row: dataRows.length + formattedColumnHeaders.length,
        });
        rawData.push({
          value,
          depth,
          type: 'rowHeader',
        });

        rowRecurse(rowGroups[key], depth + 1, rowHeaders);
      }
    }
  }

  let dataGroups = [];

  if (rows.length > 0) {
    for (let i = 0; i < rows.length; i++) {
      // possible memoization opportunity
      rowRecurse(groupByCategories(data, rows.slice(0, i + 1)), 0, dataGroups);
      dataGroups = Object.assign([], dataRows);
      if (i + 1 < rows.length) {
        dataRows = [];
        rawData = [];
        prevKey = null;
      }
    }
  } else if (cols.length > 0) {
    for (let i = 0; i < cols.length; i++) {
      rowRecurse(groupByCategories(data, cols.slice(0, i + 1)), 0, dataGroups);
      dataGroups = Object.assign([], dataRows);
      if (i + 1 < cols.length) {
        dataRows = [];
        rawData = [];
        prevKey = null;
      }
    }
  } else {
    dataRows.push({
      value: [rowHeader, accumulator(data, accCatOrCB, accTypeOrInitVal)],
      type: 'data',
      row: 1,
      depth: 0,
    });
    rawData = data.map((value) => {
      return {
        value,
        type: 0,
        row: 1,
        depth: 0,
      };
    });
  }

  function tableRowAggregator(rows) {
    const filteredRows = rows.reduce((acc, { type, value }) => {
      if (acc.length === 0) {
        acc = Array(value.length).fill([]);
      }

      if (type === 'data') {
        value.forEach((valueElem, i) => {
          if (Array.isArray(valueElem)) {
            acc[i] = acc[i].concat(valueElem);
          }
        });
      }

      return acc;
    }, []);

    return filteredRows.map((accumulatedRawData) => {
      if (accumulatedRawData.length > 0) {
        return accumulator(accumulatedRawData, accCatOrCB, accTypeOrInitVal);
      }

      return '';
    });
  }

  function tableColumnAggregator(rows) {
    const filteredRows = rows.reduce((acc, { type, value }) => {
      if (type === 'data') {
        const i = acc.length;

        acc[i] = [];
        value.forEach((valueElem) => {
          if (Array.isArray(valueElem)) {
            acc[i] = acc[i].concat(valueElem);
          }
        });
      } else if (type === 0) {
        // TODO: Investigate why type is coming up as 0
        // happens when aggregating [] for rows and [] for cols
        acc.push(value);
      }

      return acc;
    }, []);

    return filteredRows.map((accumulatedRawData) => {
      if (accumulatedRawData.length > 0) {
        return accumulator(accumulatedRawData, accCatOrCB, accTypeOrInitVal);
      }

      return '';
    });
  }

  const columnAggregations = tableColumnAggregator(rawData);
  const accumulatedRows = {
    value: tableRowAggregator(rawData),
    type: 'aggregated',
  };

  const table = formattedColumnHeaders.concat(dataRows, accumulatedRows)
    .map((tableRow, i) => {
      if (tableRow.type === 'data') {
        tableRow.value = tableRow.value.concat(columnAggregations.splice(0, 1)[0]);
      } else {
        tableRow.value = tableRow.value.concat(i === 0 ? 'aggregated' : '');
      }

      return tableRow;
    });

  return {
    table,
    rawData: formattedColumnHeaders.concat(rawData),
  };
}
<commit_msg>deep cloning column headers before operating on value to add aggregation column<commit_after>/**
 * @file Contains logic to actually pivot data
*/

/**
 * @description Format data into an array of objects where the headers are the keys
 * @param {!Array|Object} data Array of arrays or an object
 * @returns {Array<Object>} Formatted object
*/
export function fixDataFormat(data, rows = []) {
  if (!Array.isArray(data) || !data.length) return [];

  let formattedData = [];

  if (typeof data[0] === 'object' && !Array.isArray(data[0])) {
    formattedData = data;
  } else {
    formattedData = data.reduce((dataCol, row, i, arr) => {
      if (i !== 0) {
        if (Array.isArray(row)) {
          dataCol.push(row.reduce((acc, curr, index) =>{
            acc[arr[0][index]] = curr;
            return acc;
          }, {}));
        } else {
          dataCol.push({[arr[0]]: row});
        }
      }
      return dataCol;
    }, []);
  }

  // sorting data initially to prevent changes in data ordering
  if (rows.length > 0) {
    return rows.reduceRight((acc, row) => {
      return acc.sort((a, b) => {
        return a[row] > b[row];
      });
    }, formattedData);
  }

  return formattedData.sort((a, b) => {
    return JSON.stringify(a) > JSON.stringify(b);
  });
}

/**
 * @description Groups the data into an object by the provided category
 * @param {!Array<Object>} data Array of objects
 * @param {string} groupBy Category to group by
 * @returns {Object} Each key in the object is one the category groups
*/
export function groupByCategory(data, groupBy) {
  return data.reduce((acc, curr) =>{
    const category = curr[groupBy];

    if (!acc[category]) acc[category] = [];
    acc[category].push(curr);
    return acc;
  }, {});
}

/**
 * @description Performs groupByCategory recursively (nesting objects within objects)
 * @param {!Array<Object>} data Array of objects
 * @param {Array} groups Items to categorize by
 * @param {Object} acc The accumulated results
 * @returns {Object} Deeply nested object
 * where each key is one the category groups
*/
export function groupByCategories(data, groups = [], acc = {}) {
  /**
   * base case - if data is empty
   * or if there are no more groups to group by
   * return result
  */
  if (data.length === 0 || groups.length === 0) return data;

  const groupedData = groupByCategory(data, groups[0]);
  const groupedDataKeys = Object.keys(groupedData);
  const children = Object.values(groupedData);

  for (let i = 0; i < children.length; i++) {
    acc[groupedDataKeys[i]] = groupByCategories(
        children[i], groups.slice(1), acc[groupedDataKeys[i]]);
  }

  return acc;
}

/**
 * @description Builds the column headers and a map to each header
 * @param {!Array<Object>} data Array of objects
 * @param {Array} cols Columns to pivot on
 * @param {string} firstColumn A string to place in the first column header
 * @returns {Object} columnHeaders (array of arrays) and mapToHeader (object)
*/
export function createColumnHeaders(data, cols = [], firstColumn = '') {
  if (cols.length === 0) {
    return {
      columnHeaders: [firstColumn],
      mapToHeader: null,
    };
  }

  const mapToHeader = groupByCategories(data, cols);
  const columnHeaders = [];
  let mapPos = 1;

  (function columnHeaderRecursion(data, pos = 0, headerMap) {
    /** base case - at actual data as opposed to another grouping */
    if (typeof data !== 'object' || Array.isArray(data)) return 1;

    const currKeys = Object.keys(data);
    let sumLength = 0;

    for (let i = 0; i < currKeys.length; i++) {
      const currLength = columnHeaderRecursion(
          data[currKeys[i]], pos + 1, headerMap[currKeys[i]]);

      if (Array.isArray(data[currKeys[i]])) {
        headerMap[currKeys[i]] = mapPos;
        mapPos += 1;
      }

      columnHeaders[pos] = typeof columnHeaders[pos] === 'undefined' ?
        [firstColumn].concat(Array(currLength).fill(currKeys[i])) :
        columnHeaders[pos].concat(Array(currLength).fill(currKeys[i]));

      sumLength += currLength;
    }

    return sumLength;

  }(mapToHeader, 0, mapToHeader));

  return {
    columnHeaders,
    mapToHeader,
  };
}

/**
 * @description Reduces an array of values to a result
 * @param {!Array} arr The array to reduce
 * @param {?requestCallback|string} accCat Callback, category to reduce, or null
 * @param {string} accType Reduce type (count, average, min, max, etc) or initial value
 * @param {*} accValue Initial value - string, number, array, or object
 * @returns {*} Reduced value
 * @todo Move accumulator to its own file since it will continue to grow
*/
export function accumulator(arr, accCat, accType, accValue) {
  if (typeof accCat === 'undefined') accType = 'count';
  else if (typeof accCat === 'function') accValue = accType || 0;

  return arr.reduce((acc, curr, index, array) => {
    if (typeof accCat === 'function') return accCat(acc, curr, index, array);

    switch (accType) {
      case ('average'): {
        acc += Number(curr[accCat]) / arr.length;

        return acc;
      }

      case ('count'): {
        acc += 1;

        return acc;
      }

      case ('min'): {
        if (index === 0) acc = Number(curr[accCat]);
        else if (curr[accCat] < acc) acc = Number(curr[accCat]);

        return acc;
      }

      case ('max'): {
        if (index === 0) acc = Number(curr[accCat]);
        else if (curr[accCat] > acc) acc = Number(curr[accCat]);

        return acc;
      }

      case ('sum'): {
        acc += Number(curr[accCat]);

        return acc;
      }

      default: {
        acc += 1;

        return acc;
      }
    }
  }, accValue || 0);
}

/**
 * @description Check that categories to pivot on actually exist
 * @param {!Object} actualCats Categories that actually exist in the data
 * @param {!Array<string>} selectedCats Categories to pivot selected by user
 * @throws Will throw an error if the category does not exist
*/
export function checkPivotCategories(actualCats, selectedCats) {
  const errMessage = selectedCats.filter((selectedCat) => {
    return !(selectedCat in actualCats);
  });

  if (errMessage.length > 0) {
    throw new Error('Check that these selected pivot categories exist: ' +
      errMessage.join(', '));
  }
}

export function tableCreator(data, rows = [], cols = [], accCatOrCB,
  accTypeOrInitVal, rowHeader) {

  /** if data is empty, return empty array */
  if (data.length === 0) {
    return {
      rawData: [],
      table: [],
    };
  };

  /** if rows/cols are not arrays, return throw an error */
  if (!Array.isArray(rows) || !Array.isArray(cols)) {
    throw new Error('rowsToPivot and colsToPivot must be of type array');
  }

  checkPivotCategories(data[0], rows);
  checkPivotCategories(data[0], cols);

  if (typeof rowHeader === 'undefined') {
    rowHeader = typeof accCatOrCB !== 'function' ?
      `${accTypeOrInitVal} ${accCatOrCB}` :
      'Custom Agg';
  }

  const columnData = createColumnHeaders(data, cols, rowHeader);
  const columnHeaders = Array.isArray(columnData.columnHeaders[0]) ?
    columnData.columnHeaders :
    [columnData.columnHeaders.concat(rowHeader)];
  const mapToHeader = columnData.mapToHeader;
  const headerLength = columnHeaders[0].length;
  const formattedColumnHeaders = columnHeaders.map((value, depth) => {
    return {
      value,
      depth,
      type: 'colHeader',
      row: depth,
    };
  });

  let dataRows = [];
  let rawData = [];
  let prevKey = null;

  function rowRecurse(rowGroups, depth, rowHeaders = []) {
    for (const key in rowGroups) {
      if (Array.isArray(rowGroups[key])) {
        const recursedData = groupByCategories(rowGroups[key], cols);

        prevKey = null;

        (function recurseThroughMap(dataPos, map) {
          if (Array.isArray(dataPos)) {
            if (key === prevKey) {
              const datum = dataRows[dataRows.length - 1].value;

              datum[map] = accumulator(dataPos, accCatOrCB, accTypeOrInitVal);
              dataRows[dataRows.length - 1].value = datum;

              const rawDataDatum = rawData[rawData.length - 1].value;

              rawDataDatum[map] = dataPos;
              rawData[rawData.length - 1].value = rawDataDatum;
            } else {
              prevKey = key;
              const datum = [key].concat(
                Array(map - 1).fill(''),
                accumulator(dataPos, accCatOrCB, accTypeOrInitVal),
                Array(headerLength - (map + 1)).fill('')
              );
              const rawDataDatum = [key].concat(
                Array(map - 1).fill(''),
                [dataPos],
                Array(headerLength - (map + 1)).fill('')
              );

              rawData.push({
                value: rawDataDatum,
                type: 'data',
                depth,
              });
              dataRows.push({
                value: datum,
                type: 'data',
                depth,
                row: dataRows.length + formattedColumnHeaders.length,
              });
            }
          } else {
            for (const innerKey in dataPos) {
              recurseThroughMap(dataPos[innerKey], map[innerKey]);
            }
          }
        })(recursedData, mapToHeader || 1);

      } else {
        const rowHeaderValue = rowHeaders.shift();
        const value = rowHeaderValue ?
            rowHeaderValue.value :
            [key].concat(Array(headerLength - 1).fill(''));

        dataRows.push({
          value,
          depth,
          type: 'rowHeader',
          row: dataRows.length + formattedColumnHeaders.length,
        });
        rawData.push({
          value,
          depth,
          type: 'rowHeader',
        });

        rowRecurse(rowGroups[key], depth + 1, rowHeaders);
      }
    }
  }

  let dataGroups = [];

  if (rows.length > 0) {
    for (let i = 0; i < rows.length; i++) {
      // possible memoization opportunity
      rowRecurse(groupByCategories(data, rows.slice(0, i + 1)), 0, dataGroups);
      dataGroups = Object.assign([], dataRows);
      if (i + 1 < rows.length) {
        dataRows = [];
        rawData = [];
        prevKey = null;
      }
    }
  } else if (cols.length > 0) {
    for (let i = 0; i < cols.length; i++) {
      rowRecurse(groupByCategories(data, cols.slice(0, i + 1)), 0, dataGroups);
      dataGroups = Object.assign([], dataRows);
      if (i + 1 < cols.length) {
        dataRows = [];
        rawData = [];
        prevKey = null;
      }
    }
  } else {
    dataRows.push({
      value: [rowHeader, accumulator(data, accCatOrCB, accTypeOrInitVal)],
      type: 'data',
      row: 1,
      depth: 0,
    });
    rawData = data.map((value) => {
      return {
        value,
        type: 0,
        row: 1,
        depth: 0,
      };
    });
  }

  function tableRowAggregator(rows) {
    const filteredRows = rows.reduce((acc, { type, value }) => {
      if (acc.length === 0) {
        acc = Array(value.length).fill([]);
      }

      if (type === 'data') {
        value.forEach((valueElem, i) => {
          if (Array.isArray(valueElem)) {
            acc[i] = acc[i].concat(valueElem);
          }
        });
      }

      return acc;
    }, []);

    return filteredRows.map((accumulatedRawData) => {
      if (accumulatedRawData.length > 0) {
        return accumulator(accumulatedRawData, accCatOrCB, accTypeOrInitVal);
      }

      return '';
    });
  }

  function tableColumnAggregator(rows) {
    const filteredRows = rows.reduce((acc, { type, value }) => {
      if (type === 'data') {
        const i = acc.length;

        acc[i] = [];
        value.forEach((valueElem) => {
          if (Array.isArray(valueElem)) {
            acc[i] = acc[i].concat(valueElem);
          }
        });
      } else if (type === 0) {
        // TODO: Investigate why type is coming up as 0
        // happens when aggregating [] for rows and [] for cols
        acc.push(value);
      }

      return acc;
    }, []);

    return filteredRows.map((accumulatedRawData) => {
      if (accumulatedRawData.length > 0) {
        return accumulator(accumulatedRawData, accCatOrCB, accTypeOrInitVal);
      }

      return '';
    });
  }

  const columnAggregations = tableColumnAggregator(rawData);
  const accumulatedRows = {
    value: tableRowAggregator(rawData),
    type: 'aggregated',
  };

  // TODO: fix this horrible hack of deep cloning object
  const colHeadersCopy = JSON.parse(JSON.stringify(formattedColumnHeaders));

  let counter = 0;
  const table = colHeadersCopy.concat(dataRows, accumulatedRows)
    .map((tableRow, i) => {
      if (tableRow.type === 'data') {
        tableRow.value = tableRow.value.concat(columnAggregations[counter]);
        counter += 1;
      } else {
        tableRow.value = tableRow.value.concat(i === 0 ? 'aggregated' : '');
      }

      return tableRow;
    });

  return {
    table,
    rawData: formattedColumnHeaders.concat(rawData),
  };
}
<|endoftext|>