/* global IITC -- eslint */
/**
* @file This file contains the code for displaying and handling the regional scoreboard.
* @module region_scoreboard
*/
/**
* Sets up and manages the main dialog for the regional scoreboard.
*
* @function RegionScoreboardSetup
* @returns {Function} A setup function to initialize the scoreboard.
*/
window.RegionScoreboardSetup = (function() {
var mainDialog;
var regionScore;
var timer;
/**
* Constructs a RegionScore object from server results. Contains methods to process and retrieve score data.
*
* @class
* @name RegionScore
* @param {Object} serverResult - The data returned from the server for regional scores.
*/
function RegionScore(serverResult) {
this.ori_data = serverResult;
this.topAgents = serverResult.topAgents;
this.regionName = serverResult.regionName;
this.gameScore = serverResult.gameScore;
this.median=[-1,-1,-1];
this.CP_COUNT = 35;
this.CP_DURATION = 5*60*60*1000;
this.CYCLE_DURATION = this.CP_DURATION * this.CP_COUNT;
this.checkpoints = [];
this.hasNoTopAgents = function() {
return this.topAgents.length===0;
};
this.getAvgScore = function(faction) {
return parseInt(this.gameScore[ faction===TEAM_ENL? 0:1 ]);
};
this.getAvgScoreMax = function() {
return Math.max(this.getAvgScore(TEAM_ENL), this.getAvgScore(TEAM_RES), 1);
};
this.getCPScore = function(cp) {
return this.checkpoints[cp];
};
this.getScoreMax = function(min_value) {
var max = min_value || 0;
for (var i=1; i<this.checkpoints.length; i++) {
var cp = this.checkpoints[i];
max = Math.max(max,cp[0],cp[1]);
}
return max;
};
this.getCPSum = function() {
var sums=[0,0];
for (var i=1; i<this.checkpoints.length; i++) {
sums[0] += this.checkpoints[i][0];
sums[1] += this.checkpoints[i][1];
}
return sums;
};
this.getAvgScoreAtCP = function(faction, cp_idx) {
var idx = faction===TEAM_RES? 1:0;
var score = 0;
var count = 0;
var cp_len = Math.min(cp_idx,this.checkpoints.length);
for (var i=1; i<=cp_len; i++) {
if (this.checkpoints[i] !== undefined) {
score += this.checkpoints[i][idx];
count++;
}
}
if (count < cp_idx) {
score += this.getScoreMedian(faction)*(cp_idx-count);
}
return Math.floor(score / cp_idx);
};
this.getScoreMedian = function(faction) {
if (this.median[faction]<0) {
var idx = faction===TEAM_RES? 1:0;
var values = this.checkpoints.map( function(val) { return val[idx];} );
values = values.filter(function(n) { return n !== undefined; });
this.median[faction] = this.findMedian(values);
}
return this.median[faction];
};
this.findMedian = function(values) {
var len = values.length;
var rank = Math.floor((len-1)/2);
if (len===0) return 0;
var l=0, m=len-1;
var b,i,j,x;
while (l<m) {
x=values[rank];
i=l;
j=m;
do {
while (values[i]<x) i++;
while (x<values[j]) j--;
if (i<=j) {
b = values[i];
values[i] = values[j];
values[j] = b;
i++ ; j-- ;
}
} while (i<=j);
if (j<rank) l=i ;
if (rank<i) m=j ;
}
return values[rank];
};
this.getLastCP = function() {
if (this.checkpoints.length===0) return 0;
return this.checkpoints.length-1;
};
this.getCycleEnd = function() {
return this.getCheckpointEnd(this.CP_COUNT);
};
this.getCheckpointEnd = function(cp) {
return new Date(this.cycleStartTime.getTime() + this.CP_DURATION * cp);
};
for (var i=0; i<serverResult.scoreHistory.length; i++) {
var h = serverResult.scoreHistory[i];
this.checkpoints[parseInt(h[0])] = [parseInt(h[1]), parseInt(h[2])];
}
this.cycleStartTime = new Date(Math.floor(Date.now() / this.CYCLE_DURATION) * this.CYCLE_DURATION);
}
function showDialog() {
var latLng = map.getCenter();
var latE6 = Math.round(latLng.lat*1E6);
var lngE6 = Math.round(latLng.lng*1E6);
showRegion(latE6, lngE6);
}
/*
function showScoreOf (region) {
const latlng = regionToLatLong(region);
const latE6 = Math.round(latLng.lat*1E6);
const lngE6 = Math.round(latLng.lng*1E6);
showRegion(latE6,lngE6);
}
*/
function showRegion(latE6,lngE6) {
var text = 'Loading regional scores...';
if (window.useAppPanes()) {
var style = 'position: absolute; top: 0; width: 100%; max-width: 412px';
mainDialog = $('<div>',{style: style}).html(text).appendTo(document.body);
} else {
mainDialog = dialog({
title: 'Region scores',
html: text,
width: 450,
height: 340,
closeCallback: onDialogClose
});
}
window.postAjax('getRegionScoreDetails', {latE6:latE6, lngE6:lngE6},
onRequestSuccess,
onRequestFailure);
}
function onRequestFailure() {
mainDialog.html('Failed to load region scores - try again');
}
function onRequestSuccess(data) {
if (data.result === undefined) {
return onRequestFailure();
}
regionScore = new RegionScore(data.result);
updateDialog();
startTimer();
}
function updateDialog(logscale) {
mainDialog.html(
'<div class="cellscore">' +
'<b>Region scores for ' + regionScore.regionName + '</b>' +
'<div class="historychart">' + createResults() + HistoryChart(regionScore, logscale) + '</div>' +
'<b>Checkpoint overview</b><div>' + createHistoryTable() + '</div>' +
'<b>Top agents</b><div>' + createAgentTable() + '</div>' +
'</div>' +
createTimers() );
setupToolTips();
var tooltip = createResultTooltip();
$('#overview', mainDialog).tooltip({
content: convertTextToTableMagic(tooltip)
});
$('.cellscore', mainDialog).accordion({
header: 'b',
heightStyle: 'fill'
});
$('input.logscale', mainDialog).change(function() {
var input = $(this);
updateDialog(input.prop('checked'));
});
}
function setupToolTips() {
$('g.checkpoint', mainDialog).each(function(i, elem) {
elem = $(elem);
function formatScore(idx, score_now, score_last) {
if (!score_now[idx]) return '';
var res = digits(score_now[idx]);
if (score_last && score_last[idx]) {
var delta = score_now[idx]-score_last[idx];
res += '\t(';
if (delta>0) res += '+';
res += digits(delta) + ')';
}
return res;
}
var tooltip;
var cp = parseInt(elem.attr('data-cp'));
if (cp) {
var score_now = regionScore.getCPScore(cp);
var score_last = regionScore.getCPScore(cp-1);
var enl_str = score_now ? '\nEnl:\t' + formatScore(0,score_now,score_last) : '';
var res_str = score_now ? '\nRes:\t' + formatScore(1,score_now,score_last) : '';
tooltip = 'CP:\t' + cp + '\t-\t' + formatDayHours(regionScore.getCheckpointEnd(cp)) +
'\n<hr>' + enl_str + res_str;
}
elem.tooltip({
content: convertTextToTableMagic(tooltip),
position: {my: 'center bottom', at: 'center top-10'},
tooltipClass: 'checkpointtooltip',
show: 100
});
});
}
function onDialogClose() {
stopTimer();
}
function createHistoryTable() {
var _invert = PLAYER.team === 'RESISTANCE';
function order (_1, _2) {
return (_invert ? [_2, _1] : [_1, _2]).join('');
}
var enl = { class: window.TEAM_TO_CSS[window.TEAM_ENL], name: window.TEAM_NAMES[window.TEAM_ENL]};
var res = { class: window.TEAM_TO_CSS[window.TEAM_RES], name: window.TEAM_NAMES[window.TEAM_RES]};
var table = '<table class="checkpoint_table"><thead>' +
'<tr><th>CP</th><th>Time</th>' + order('<th>'+enl.name+'</th>','<th>'+res.name+'</th>') + '</tr>';
var total = regionScore.getCPSum();
table += '<tr class="cp_total"><th></th><th></th>' +
order(
'<th class="' + enl.class + '">' + digits(total[0]) + '</th>',
'<th class="' + res.class + '">' + digits(total[1]) + '</th>'
) + '</tr></thead>';
for (var cp=regionScore.getLastCP(); cp>0; cp--) {
var score = regionScore.getCPScore(cp);
var class_e = score[0] > score[1] ? ' class="' + enl.class + '"' : '';
var class_r = score[1] > score[0] ? ' class="' + res.class + '"' : '';
table += '<tr>' +
'<td>' + cp + '</td>' +
'<td>' + formatDayHours(regionScore.getCheckpointEnd(cp)) + '</td>' +
order(
'<td' + class_e + '>' + digits(score[0]) + '</td>',
'<td' + class_r + '>' + digits(score[1]) + '</td>'
) + '</tr>';
}
table += '</table>';
return table;
}
function createAgentTable() {
var agentTable = '<table><tr><th>#</th><th>Agent</th></tr>';
for (var i=0; i<regionScore.topAgents.length; i++) {
var agent = regionScore.topAgents[i];
agentTable += '<tr>' +
'<td>' + (i+1) + '</td>' +
'<td class="nickname ' + (agent.team==='RESISTANCE'?'res':'enl') + '">' + agent.nick + '</td></tr>';
}
if (regionScore.hasNoTopAgents()) {
agentTable += '<tr><td colspan="2"><i>no top agents</i></td></tr>';
}
agentTable += '</table>';
return agentTable;
}
function createResults() {
var maxAverage = regionScore.getAvgScoreMax();
var order = (PLAYER.team === 'RESISTANCE' ? [TEAM_RES,TEAM_ENL]:[TEAM_ENL,TEAM_RES]);
var result = '<table id="overview" title="">';
for (var t=0; t<2; t++) {
var faction = order[t];
var team = window.TEAM_NAMES[faction];
var teamClass = window.TEAM_TO_CSS[faction];
var teamCol = COLORS[faction];
var barSize = Math.round(regionScore.getAvgScore(faction)/maxAverage*100);
result += '<tr><th class="' + teamClass + '">' + team + '</th>' +
'<td class="' + teamClass + '">' + digits(regionScore.getAvgScore(faction)) + '</td>' +
'<td style="width:100%"><div style="background:' + teamCol + '; width: ' + barSize + '%; height: 1.3ex; border: 2px outset ' + teamCol + '; margin-top: 2px"> </td>' +
'<td class="' + teamClass + '"><small>( ' + digits(regionScore.getAvgScoreAtCP(faction,35)) + ' )</small></td>' +
'</tr>';
}
return result + '</table>';
}
function createResultTooltip() {
var e_res = regionScore.getAvgScoreAtCP(TEAM_RES,regionScore.CP_COUNT);
var e_enl = regionScore.getAvgScoreAtCP(TEAM_ENL,regionScore.CP_COUNT);
var loosing_faction = e_res<e_enl ? TEAM_RES : TEAM_ENL;
var order = (loosing_faction === TEAM_ENL ? [TEAM_RES,TEAM_ENL]:[TEAM_ENL,TEAM_RES]);
function percentToString(score,total) {
if (total===0) return '50%';
return (Math.round( score/total * 10000 )/100) + '%';
}
function currentScore() {
var res='Current:\n';
var total = regionScore.getAvgScore(TEAM_RES) + regionScore.getAvgScore(TEAM_ENL);
for (var t=0; t<2; t++) {
var faction = order[t];
var score = regionScore.getAvgScore(faction);
res += window.TEAM_NAMES[faction] + '\t' +
digits(score) + '\t' +
percentToString(score,total) + '\n';
}
return res;
}
function estimatedScore() {
var res ='<hr>Estimated:\n';
var total = e_res + e_enl;
for (var t=0; t<2; t++) {
var faction = order[t];
var score = regionScore.getAvgScoreAtCP(faction,regionScore.CP_COUNT);
res += window.TEAM_NAMES[faction] + '\t' +
digits(score) + '\t' +
percentToString(score,total) + '\n';
}
return res;
}
function requiredScore() {
var res='';
var required_mu = Math.abs(e_res-e_enl) * regionScore.CP_COUNT + 1;
res += '<hr>\n';
res += window.TEAM_NAMES[loosing_faction] + ' requires:\t' + digits(Math.ceil(required_mu)) + ' \n';
res += 'Checkpoint(s) left:\t' + (regionScore.CP_COUNT-regionScore.getLastCP()) + ' \n';
return res;
}
return currentScore() + estimatedScore() + requiredScore();
}
function createTimers() {
var nextcp = regionScore.getCheckpointEnd( regionScore.getLastCP() + 1 );
var endcp = regionScore.getCycleEnd();
return '<div class="checkpoint_timers"><table><tr>' +
'<td>Next CP at: ' + formatHours(nextcp) + ' (in <span id="cycletimer"></span>)</td>' +
'<td>Cycle ends: ' + formatDayHours(endcp) + '</td>' +
'</tr></table></div>';
}
function startTimer() {
stopTimer();
timer = window.setInterval(onTimer, 1000);
onTimer();
}
function stopTimer() {
if (timer) {
window.clearInterval(timer);
timer = undefined;
}
}
function onTimer() {
var d = regionScore.getCheckpointEnd(regionScore.getLastCP() + 1) - (new Date());
$('#cycletimer',mainDialog).html(formatMinutes( Math.max(0,Math.floor(d/1000))) );
}
function formatMinutes(sec) {
var hours = Math.floor(sec / 3600);
var minutes = Math.floor((sec % 3600) / 60);
sec = sec % 60;
var time='';
time += hours + ':';
if (minutes<10) time += '0';
time += minutes;
time += ':';
if (sec<10) time += '0';
time += sec;
return time;
}
function formatHours(time) {
return ('0' + time.getHours()).slice(-2) + ':00';
}
function formatDayHours(time) {
return ('0' + time.getDate()).slice(-2) + '.' + ('0' + (time.getMonth() + 1)).slice(-2) + ' ' + ('0' + time.getHours()).slice(-2) + ':00';
}
return function setup() {
if (window.useAppPanes()) {
app.addPane('regionScoreboard', 'Region scores', 'ic_action_view_as_list');
addHook('paneChanged', function (pane) {
if (pane === 'regionScoreboard') {
showDialog();
} else if (mainDialog) {
mainDialog.remove();
}
});
} else {
IITC.toolbox.addButton({
id: 'scoreboard',
label: 'Region scores',
title: 'View regional scoreboard',
action: showDialog,
});
}
}
}());
/**
* Creates an SVG-based history chart for regional scores.
*
* @function HistoryChart
* @param {RegionScore} _regionScore - The RegionScore object containing score data.
* @param {boolean} logscale - Whether to use logarithmic scale for the chart.
* @returns {string} An SVG string representing the history chart.
*/
var HistoryChart = (function() {
var regionScore;
var scaleFct;
var logscale;
var svgTickText;
function create(_regionScore,logscale) {
regionScore = _regionScore;
var max = regionScore.getScoreMax(10); //NOTE: ensure a min of 10 for the graph
max *= 1.09; // scale up maximum a little, so graph isn't squashed right against upper edge
setScaleType(max,logscale);
svgTickText = [];
// svg area 400x130. graph area 350x100, offset to 40,10
var svg = '<div><svg width="400" height="133" style="margin-left: 10px;">' +
svgBackground() +
svgAxis(max) +
svgAveragePath() +
svgFactionPath() +
svgCheckPointMarkers() +
svgTickText.join('') +
'<foreignObject height="18" width="60" y="113" x="0" class="node"><label title="Logarithmic scale">' +
'<input type="checkbox" class="logscale"' + (logscale?' checked':'') + '/>' +
'log</label></foreignObject>' +
'</svg></div>';
return svg;
}
function svgFactionPath() {
var svgPath = '';
for (var t=0; t<2; t++) {
var col = getFactionColor(t);
var teamPaths = [];
for (var cp=1; cp<=regionScore.getLastCP(); cp++) {
var score = regionScore.getCPScore(cp);
if (score !== undefined) {
var x = cp * 10 + 40;
teamPaths.push(x + ',' + scaleFct(score[t]));
}
}
if (teamPaths.length > 0) {
svgPath += '<polyline points="' + teamPaths.join(' ') + '" stroke="' + col + '" fill="none" />';
}
}
return svgPath;
}
function svgCheckPointMarkers() {
var markers = '';
var col1 = getFactionColor(0);
var col2 = getFactionColor(1);
for (var cp=1; cp<=regionScore.CP_COUNT; cp++) {
var scores = regionScore.getCPScore(cp);
markers +=
'<g title="dummy" class="checkpoint" data-cp="' + cp + '">' +
'<rect x="' + (cp*10+35) + '" y="10" width="10" height="100" fill="black" fill-opacity="0" />';
if (scores) {
markers +=
'<circle cx="' + (cp*10+40) + '" cy="' + scaleFct(scores[0]) + '" r="3" stroke-width="1" stroke="' + col1 + '" fill="' + col1 + '" fill-opacity="0.5" />' +
'<circle cx="' + (cp*10+40) + '" cy="' + scaleFct(scores[1]) + '" r="3" stroke-width="1" stroke="' + col2 + '" fill="' + col2 + '" fill-opacity="0.5" />';
}
markers += '</g>';
}
return markers;
}
function svgBackground() {
return '<rect x="0" y="1" width="400" height="132" stroke="#FFCE00" fill="#08304E" />';
}
function svgAxis(max) {
return '<path d="M40,110 L40,10 M40,110 L390,110" stroke="#fff" />' + createTicks(max);
}
function createTicks(max) {
var ticks = createTicksHorz();
function addVTick(i) {
var y = scaleFct(i);
ticks.push('M40,' + y + ' L390,' + y);
svgTickText.push('<text x="35" y="' + y + '" font-size="12" font-family="Roboto, Helvetica, sans-serif" text-anchor="end" fill="#fff">' + formatNumber(i) + '</text>');
}
// vertical
// first we calculate the power of 10 that is smaller than the max limit
var vtickStep = Math.pow(10,Math.floor(Math.log10(max)));
if (logscale) {
for (var i=0; i<4; i++) {
addVTick(vtickStep);
vtickStep /= 10;
}
} else {
// this could be between 1 and 10 grid lines - so we adjust to give nicer spacings
if (vtickStep < (max/5)) {
vtickStep *= 2;
} else if (vtickStep > (max/2)) {
vtickStep /= 2;
}
for (var ti=vtickStep; ti<=max; ti+=vtickStep) {
addVTick(ti);
}
}
return ('<path d="' + ticks.join(' ') + '" stroke="#fff" opacity="0.3" />');
}
function createTicksHorz() {
var ticks = [];
for (var i=5; i<=35; i+=5) {
var x = i * 10 + 40;
ticks.push('M' + x + ',10 L' + x + ',110');
svgTickText.push('<text x="' + x + '" y="125" font-size="12" font-family="Roboto, Helvetica, sans-serif" text-anchor="middle" fill="#fff">' + i + '</text>');
}
return ticks;
}
function svgAveragePath() {
var path='';
for (var faction=1; faction<3; faction++) {
var col = COLORS[faction];
var points=[];
for (var cp=1; cp<=regionScore.CP_COUNT; cp++) {
var score = regionScore.getAvgScoreAtCP(faction, cp);
var x = cp * 10 + 40;
var y = scaleFct(score);
points.push(x + ',' + y);
}
path += '<polyline points="' + points.join(' ') + '" stroke="' + col + '" stroke-dasharray="3,2" opacity="0.8" fill="none"/>';
}
return path;
}
function setScaleType(max,useLogScale) {
logscale = useLogScale;
if (useLogScale) {
if (!Math.log10)
Math.log10 = function(x) { return Math.log(x) / Math.LN10; };
// 0 cannot be displayed on a log scale, so we set the minimum to 0.001 and divide by lg(0.001)=-3
scaleFct = function(y) { return Math.round(10 - Math.log10(Math.max(0.001,y/max)) / 3 * 100); };
} else {
scaleFct = function(y) { return Math.round(110-y/max*100); };
}
}
function getFactionColor(t) {
return (t===0 ? COLORS[TEAM_ENL] : COLORS[TEAM_RES]);
}
function formatNumber(num) {
return (num>=1000000000 ? (num/1000000000) + 'B' :
num>=1000000 ? (num/1000000) + 'M' :
num>=1000 ? (num/1000) + 'k' :
num);
}
return create;
}());