665 lines
26 KiB
JavaScript
665 lines
26 KiB
JavaScript
/*
|
|
pyRCV2: Preferential vote counting
|
|
Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
function clickAdvancedOptions() {
|
|
if (document.getElementById('divAdvancedOptions').style.display === 'none') {
|
|
document.getElementById('divAdvancedOptions').style.display = 'grid';
|
|
document.getElementById('btnAdvancedOptions').innerHTML = 'Hide advanced options';
|
|
} else {
|
|
document.getElementById('divAdvancedOptions').style.display = 'none';
|
|
document.getElementById('btnAdvancedOptions').innerHTML = 'Show advanced options';
|
|
}
|
|
}
|
|
|
|
function changePreset() {
|
|
if (document.getElementById('selPreset').value === 'scottish') {
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
document.getElementById('selQuota').value = 'droop';
|
|
document.getElementById('selQuotaMode').value = 'static';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = false;
|
|
document.getElementById('chkDeferSurpluses').checked = false;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '5';
|
|
document.getElementById('txtPPDP').value = '2';
|
|
document.getElementById('chkRoundQuota').checked = true;
|
|
document.getElementById('txtRoundQuota').value = '0';
|
|
document.getElementById('chkRoundVotes').checked = false;
|
|
document.getElementById('chkRoundTVs').checked = false;
|
|
document.getElementById('chkRoundWeights').checked = false;
|
|
document.getElementById('selSurplus').value = 'size';
|
|
document.getElementById('selTransfers').value = 'wig';
|
|
document.getElementById('selPapers').value = 'both';
|
|
document.getElementById('selExclusion').value = 'one_round';
|
|
document.getElementById('selTies').value = 'backwards_random';
|
|
} else if (document.getElementById('selPreset').value === 'stvc') {
|
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
|
document.getElementById('selQuota').value = 'droop_exact';
|
|
document.getElementById('selQuotaMode').value = 'progressive';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
document.getElementById('chkDeferSurpluses').checked = false;
|
|
document.getElementById('selNumbers').value = 'rational';
|
|
document.getElementById('txtPPDP').value = '2';
|
|
document.getElementById('chkRoundQuota').checked = false;
|
|
document.getElementById('chkRoundVotes').checked = false;
|
|
document.getElementById('chkRoundTVs').checked = false;
|
|
document.getElementById('chkRoundWeights').checked = false;
|
|
document.getElementById('selSurplus').value = 'size';
|
|
document.getElementById('selTransfers').value = 'wig';
|
|
document.getElementById('selPapers').value = 'both';
|
|
document.getElementById('selExclusion').value = 'one_round';
|
|
document.getElementById('selTies').value = 'backwards_random';
|
|
} else if (document.getElementById('selPreset').value === 'senate') {
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
document.getElementById('selQuota').value = 'droop';
|
|
document.getElementById('selQuotaMode').value = 'static';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
document.getElementById('chkDeferSurpluses').checked = false;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '5';
|
|
document.getElementById('txtPPDP').value = '0';
|
|
document.getElementById('chkRoundQuota').checked = true;
|
|
document.getElementById('txtRoundQuota').value = '0';
|
|
document.getElementById('chkRoundVotes').checked = true;
|
|
document.getElementById('txtRoundVotes').value = '0';
|
|
document.getElementById('chkRoundTVs').checked = false;
|
|
document.getElementById('chkRoundWeights').checked = false;
|
|
document.getElementById('selSurplus').value = 'order';
|
|
document.getElementById('selTransfers').value = 'uig';
|
|
document.getElementById('selPapers').value = 'both';
|
|
document.getElementById('selExclusion').value = 'by_value';
|
|
document.getElementById('selTies').value = 'backwards_random';
|
|
} else if (document.getElementById('selPreset').value === 'meek') {
|
|
document.getElementById('selQuotaCriterion').value = 'gt';
|
|
document.getElementById('selQuota').value = 'droop_exact';
|
|
document.getElementById('selQuotaMode').value = 'progressive';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = false;
|
|
document.getElementById('chkDeferSurpluses').checked = false;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '9';
|
|
document.getElementById('txtPPDP').value = '2';
|
|
document.getElementById('chkRoundQuota').checked = false;
|
|
document.getElementById('chkRoundVotes').checked = false;
|
|
document.getElementById('chkRoundTVs').checked = false;
|
|
document.getElementById('chkRoundWeights').checked = false;
|
|
document.getElementById('selSurplus').value = 'size';
|
|
document.getElementById('selTransfers').value = 'meek';
|
|
document.getElementById('selPapers').value = 'both';
|
|
document.getElementById('selExclusion').value = 'one_round';
|
|
document.getElementById('selTies').value = 'random';
|
|
} else if (document.getElementById('selPreset').value === 'wright') {
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
document.getElementById('selQuota').value = 'droop';
|
|
document.getElementById('selQuotaMode').value = 'static';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
document.getElementById('chkDeferSurpluses').checked = false;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '5';
|
|
document.getElementById('txtPPDP').value = '2';
|
|
document.getElementById('chkRoundQuota').checked = true;
|
|
document.getElementById('txtRoundQuota').value = '0';
|
|
document.getElementById('chkRoundVotes').checked = false;
|
|
document.getElementById('chkRoundTVs').checked = false;
|
|
document.getElementById('chkRoundWeights').checked = false;
|
|
document.getElementById('selSurplus').value = 'size';
|
|
document.getElementById('selTransfers').value = 'wig';
|
|
document.getElementById('selPapers').value = 'both';
|
|
document.getElementById('selExclusion').value = 'wright';
|
|
document.getElementById('selTies').value = 'backwards_random';
|
|
} else if (document.getElementById('selPreset').value === 'prsa77') {
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
document.getElementById('selQuota').value = 'droop';
|
|
document.getElementById('selQuotaMode').value = 'static';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = false;
|
|
document.getElementById('chkDeferSurpluses').checked = true;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '5';
|
|
document.getElementById('txtPPDP').value = '3';
|
|
document.getElementById('chkRoundQuota').checked = true;
|
|
document.getElementById('txtRoundQuota').value = '3';
|
|
document.getElementById('chkRoundVotes').checked = true;
|
|
document.getElementById('txtRoundVotes').value = '3';
|
|
document.getElementById('chkRoundTVs').checked = true;
|
|
document.getElementById('txtRoundTVs').value = '3';
|
|
document.getElementById('chkRoundWeights').checked = true;
|
|
document.getElementById('txtRoundWeights').value = '3';
|
|
document.getElementById('selSurplus').value = 'order';
|
|
document.getElementById('selTransfers').value = 'eg';
|
|
document.getElementById('selPapers').value = 'transferable';
|
|
document.getElementById('selExclusion').value = 'parcels_by_order';
|
|
document.getElementById('selTies').value = 'backwards_random';
|
|
} else if (document.getElementById('selPreset').value === 'ers97') {
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
document.getElementById('selQuota').value = 'droop_exact';
|
|
document.getElementById('selQuotaMode').value = 'ers97';
|
|
document.getElementById('chkBulkElection').checked = true;
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
document.getElementById('chkDeferSurpluses').checked = true;
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
document.getElementById('txtDP').value = '5';
|
|
document.getElementById('txtPPDP').value = '2';
|
|
document.getElementById('chkRoundQuota').checked = true;
|
|
document.getElementById('txtRoundQuota').value = '2';
|
|
document.getElementById('chkRoundVotes').checked = true;
|
|
document.getElementById('txtRoundVotes').value = '2';
|
|
document.getElementById('chkRoundTVs').checked = true;
|
|
document.getElementById('txtRoundTVs').value = '2';
|
|
document.getElementById('chkRoundWeights').checked = true;
|
|
document.getElementById('txtRoundWeights').value = '2';
|
|
document.getElementById('selSurplus').value = 'size';
|
|
document.getElementById('selTransfers').value = 'eg';
|
|
document.getElementById('selPapers').value = 'transferable';
|
|
document.getElementById('selExclusion').value = 'by_value';
|
|
document.getElementById('selTies').value = 'forwards_random';
|
|
}
|
|
}
|
|
|
|
async function clickCount() {
|
|
// Read BLT file
|
|
let bltFile = document.getElementById('bltFile').files[0];
|
|
let text = await bltFile.text();
|
|
|
|
// Initialise table rows
|
|
let tblResults = document.getElementById('result');
|
|
tblResults.innerHTML = '';
|
|
let candMap = {}; // candidate name -> rows
|
|
let divResultLogs1 = document.getElementById('resultLogs1');
|
|
let divResultLogs2 = document.getElementById('resultLogs2');
|
|
divResultLogs1.innerHTML = '';
|
|
divResultLogs2.innerHTML = '';
|
|
|
|
// Step election
|
|
let worker = new Worker('worker.js');
|
|
let election;
|
|
let trComment, trExhausted1, trExhausted2, trLTF1, trLTF2, trTotal, trQuota, trVRE;
|
|
let olLogs;
|
|
|
|
worker.onmessage = function(evt) {
|
|
if (evt.data.type === 'require_input') {
|
|
let input = window.prompt(evt.data.message);
|
|
if (input !== null) {
|
|
worker.postMessage({'type': 'require_input', 'input': input});
|
|
}
|
|
}
|
|
|
|
if (evt.data.type === 'stv_exception') {
|
|
window.alert('An error occurred while counting the votes:\n\n' + evt.data.message);
|
|
}
|
|
|
|
if (evt.data.type === 'init') {
|
|
election = evt.data.election;
|
|
|
|
// Comment row
|
|
trComment = document.createElement('tr');
|
|
let elTd = document.createElement('td');
|
|
trComment.appendChild(elTd);
|
|
tblResults.appendChild(trComment);
|
|
|
|
// Candidates
|
|
for (let candidate of election.candidates) {
|
|
let elTr1 = document.createElement('tr');
|
|
let elTr2 = document.createElement('tr');
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.setAttribute('rowspan', '2');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerText = candidate;
|
|
elTr1.appendChild(elTd);
|
|
|
|
tblResults.appendChild(elTr1);
|
|
tblResults.appendChild(elTr2);
|
|
|
|
candMap[candidate] = [elTr1, elTr2];
|
|
}
|
|
|
|
// Exhausted votes row
|
|
trExhausted1 = document.createElement('tr');
|
|
trExhausted1.classList.add('info');
|
|
trExhausted2 = document.createElement('tr');
|
|
trExhausted2.classList.add('info');
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.setAttribute('rowspan', '2');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerText = 'Exhausted';
|
|
trExhausted1.appendChild(elTd);
|
|
|
|
tblResults.appendChild(trExhausted1);
|
|
tblResults.appendChild(trExhausted2);
|
|
|
|
// Loss to fraction row
|
|
trLTF1 = document.createElement('tr');
|
|
trLTF1.classList.add('info');
|
|
trLTF2 = document.createElement('tr');
|
|
trLTF2.classList.add('info');
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.setAttribute('rowspan', '2');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerText = 'Loss to fraction';
|
|
trLTF1.appendChild(elTd);
|
|
|
|
tblResults.appendChild(trLTF1);
|
|
tblResults.appendChild(trLTF2);
|
|
|
|
// Total row
|
|
trTotal = document.createElement('tr');
|
|
trTotal.classList.add('info');
|
|
elTd = document.createElement('td');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerText = 'Total';
|
|
trTotal.appendChild(elTd);
|
|
tblResults.appendChild(trTotal);
|
|
|
|
// Quota row
|
|
trQuota = document.createElement('tr');
|
|
trQuota.classList.add('info');
|
|
elTd = document.createElement('td');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.style.borderBottom = '1px solid black';
|
|
elTd.innerText = 'Quota';
|
|
trQuota.appendChild(elTd);
|
|
tblResults.appendChild(trQuota);
|
|
|
|
// Vote required for election row
|
|
if (document.getElementById('selQuotaMode').value === 'ers97') {
|
|
trVRE = document.createElement('tr');
|
|
trVRE.classList.add('info');
|
|
elTd = document.createElement('td');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.style.borderBottom = '1px solid black';
|
|
elTd.innerText = 'Vote required for election';
|
|
trVRE.appendChild(elTd);
|
|
tblResults.appendChild(trVRE);
|
|
}
|
|
|
|
// Result logs
|
|
let elP = document.createElement('p');
|
|
let filePath = document.getElementById('bltFile').value;
|
|
filePath = filePath.substring(Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')) + 1);
|
|
if (pyRCV2version === 'GITVERSION') {
|
|
elP.innerText = 'Count computed by pyRCV2 (development version).';
|
|
} else {
|
|
elP.innerText = 'Count computed by pyRCV2 (revision ' + pyRCV2version + ').';
|
|
}
|
|
elP.innerText += ' Read ' + evt.data.total_ballots + ' ballots from ‘' + filePath + '’ for election ‘' + election.name + '’.';
|
|
elP.innerText += ' There are ' + election.candidates.length + ' candidates for ' + election.seats + ' vacancies.';
|
|
if (evt.data.options === '') {
|
|
elP.innerText += ' Counting using default options.';
|
|
} else {
|
|
elP.innerHTML += ' Counting using options <span style="font-family:monospace;">' + evt.data.options + '</span>.';
|
|
}
|
|
divResultLogs1.appendChild(elP);
|
|
|
|
elP = document.createElement('p');
|
|
elP.innerText = 'Stage comments:';
|
|
divResultLogs2.appendChild(elP);
|
|
olLogs = document.createElement('ol');
|
|
divResultLogs2.appendChild(olLogs);
|
|
}
|
|
|
|
if (evt.data.type === 'result') {
|
|
let result = evt.data.result;
|
|
|
|
// Display results
|
|
elTd = document.createElement('td');
|
|
elTd.innerText = result.stage + '. ' + result.comment;
|
|
trComment.appendChild(elTd);
|
|
|
|
for (let [candidate, countCard] of result.candidates) {
|
|
[elTr1, elTr2] = candMap[candidate];
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
|
|
elTd.classList.add('excluded');
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
|
|
elTd.classList.add('elected');
|
|
}
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerHTML = ppVotes(countCard.transfers);
|
|
elTr1.appendChild(elTd);
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
|
|
if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
|
|
elTd.classList.add('elected');
|
|
elTd.innerHTML = ppVotes(countCard.votes);
|
|
elTr1.querySelector('td:first-child').classList.add('elected');
|
|
} else {
|
|
elTr1.querySelector('td:first-child').classList.remove('elected');
|
|
|
|
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
|
|
elTd.classList.add('excluded');
|
|
elTd.innerText = 'WD';
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) {
|
|
elTd.classList.add('excluded');
|
|
elTd.innerText = 'Ex';
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
|
|
elTd.classList.add('excluded');
|
|
elTd.innerHTML = ppVotes(countCard.votes);
|
|
} else {
|
|
elTd.innerHTML = ppVotes(countCard.votes);
|
|
}
|
|
}
|
|
|
|
elTr2.appendChild(elTd);
|
|
}
|
|
|
|
// Display exhausted votes
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerHTML = ppVotes(result.exhausted.transfers);
|
|
trExhausted1.appendChild(elTd);
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.innerHTML = ppVotes(result.exhausted.votes);
|
|
trExhausted2.appendChild(elTd);
|
|
|
|
// Display loss to fraction
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerHTML = ppVotes(result.loss_fraction.transfers);
|
|
trLTF1.appendChild(elTd);
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.innerHTML = ppVotes(result.loss_fraction.votes);
|
|
trLTF2.appendChild(elTd);
|
|
|
|
// Display total
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.innerHTML = ppVotes(result.total);
|
|
trTotal.appendChild(elTd);
|
|
|
|
// Display quota
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.style.borderBottom = '1px solid black';
|
|
elTd.innerHTML = ppVotes(result.quota);
|
|
trQuota.appendChild(elTd);
|
|
|
|
// Display vote required for election
|
|
if (result.vote_required_election !== null) {
|
|
elTd = document.createElement('td');
|
|
elTd.classList.add('count');
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTd.style.borderBottom = '1px solid black';
|
|
elTd.innerHTML = ppVotes(result.vote_required_election);
|
|
trVRE.appendChild(elTd);
|
|
}
|
|
|
|
// Result logs
|
|
let elLi = document.createElement('li');
|
|
elLi.innerText = result.logs.join(' ');
|
|
olLogs.appendChild(elLi);
|
|
}
|
|
|
|
if (evt.data.type === 'done') {
|
|
let result = evt.data.result;
|
|
let winners = [];
|
|
|
|
// Display results
|
|
|
|
for (let [candidate, countCard] of result.candidates) {
|
|
[elTr1, elTr2] = candMap[candidate];
|
|
elTd = document.createElement('td');
|
|
elTd.setAttribute('rowspan', '2');
|
|
if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) {
|
|
elTd.classList.add('excluded');
|
|
elTd.innerHTML = 'Withdrawn';
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED || countCard.state === py.pyRCV2.model.CandidateState.EXCLUDING) {
|
|
elTd.classList.add('excluded');
|
|
elTd.innerHTML = 'Excluded ' + (-countCard.order_elected);
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED || countCard.state === py.pyRCV2.model.CandidateState.DISTRIBUTING_SURPLUS) {
|
|
elTd.classList.add('elected');
|
|
elTd.innerHTML = 'ELECTED ' + countCard.order_elected;
|
|
winners.append([candidate, countCard]);
|
|
}
|
|
elTd.style.borderTop = '1px solid black';
|
|
elTr1.appendChild(elTd);
|
|
}
|
|
|
|
elTd.style.borderBottom = '1px solid black';
|
|
|
|
let elP = document.createElement('p');
|
|
elP.innerText = 'Count complete. The winning candidates are, in order of election:'
|
|
divResultLogs2.appendChild(elP);
|
|
|
|
winners.sort(function(x1, x2) { return x1[1].order_elected - x2[1].order_elected; });
|
|
let elOl = document.createElement('ol');
|
|
for (let [candidate, countCard] of winners) {
|
|
let elLi = document.createElement('li');
|
|
elLi.innerText = candidate;
|
|
elOl.appendChild(elLi);
|
|
}
|
|
divResultLogs2.appendChild(elOl);
|
|
|
|
document.getElementById('printPane').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
worker.onerror = function(evt) {
|
|
window.alert('An unknown error occurred while counting the votes. More details may be available in the browser\'s developer console.');
|
|
throw evt;
|
|
}
|
|
|
|
worker.postMessage({
|
|
'type': 'init',
|
|
'data': {
|
|
'numbers': document.getElementById('selNumbers').value,
|
|
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
|
'ppDPs': parseInt(document.getElementById('txtPPDP').value),
|
|
'transfers': document.getElementById('selTransfers').value,
|
|
'options': {
|
|
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
|
'quota': document.getElementById('selQuota').value,
|
|
'quota_mode': document.getElementById('selQuotaMode').value,
|
|
'bulk_elect': document.getElementById('chkBulkElection').checked,
|
|
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
|
'defer_surpluses': document.getElementById('chkDeferSurpluses').checked,
|
|
'surplus_order': document.getElementById('selSurplus').value,
|
|
'papers': document.getElementById('selPapers').value,
|
|
'exclusion': document.getElementById('selExclusion').value,
|
|
'ties': document.getElementById('selTies').value,
|
|
'round_quota': document.getElementById('chkRoundQuota').checked ? parseInt(document.getElementById('txtRoundQuota').value) : null,
|
|
'round_votes': document.getElementById('chkRoundVotes').checked ? parseInt(document.getElementById('txtRoundVotes').value) : null,
|
|
'round_tvs': document.getElementById('chkRoundTVs').checked ? parseInt(document.getElementById('txtRoundTVs').value) : null,
|
|
'round_weights': document.getElementById('chkRoundWeights').checked ? parseInt(document.getElementById('txtRoundWeights').value) : null,
|
|
},
|
|
'seed': document.getElementById('txtSeed').value,
|
|
'data': text
|
|
}
|
|
});
|
|
|
|
// Pretty printing helper functions
|
|
let ppDPs = parseInt(document.getElementById('txtPPDP').value);
|
|
if (document.getElementById('chkRoundVotes').checked) {
|
|
let ppDPs2 = parseInt(document.getElementById('txtRoundVotes').value);
|
|
if (ppDPs2 < ppDPs) {
|
|
ppDPs = ppDPs2;
|
|
}
|
|
}
|
|
if (document.getElementById('selNumbers').value === 'fixed') {
|
|
let ppDPs2 = parseInt(document.getElementById('txtDP').value);
|
|
if (ppDPs2 < ppDPs) {
|
|
ppDPs = ppDPs2;
|
|
}
|
|
}
|
|
function ppVotes(v) {
|
|
let result = parseFloat(v).toFixed(ppDPs);
|
|
if (parseFloat(result) == 0) {
|
|
return ' '
|
|
}
|
|
if (result.indexOf('.') >= 0) {
|
|
result = result.substring(0, result.indexOf('.')) + '<sup>' + result.substring(result.indexOf('.')) + '</sup>';
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Provide a default seed
|
|
if (document.getElementById('txtSeed').value === '') {
|
|
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
|
|
let d = new Date();
|
|
document.getElementById('txtSeed').value = d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate());
|
|
}
|
|
|
|
// Split across pages when required for printing
|
|
|
|
function printResult() {
|
|
let printableWidth; // Printable width in CSS pixels
|
|
let paperSize = document.getElementById('selPaperSize').value;
|
|
if (paperSize === 'A4') {
|
|
printableWidth = (29.7 - 2) * 96 / 2.54;
|
|
} else if (paperSize === 'A3') {
|
|
printableWidth = (42.0 - 2) * 96 / 2.54;
|
|
} else if (paperSize === 'letter') {
|
|
printableWidth = (27.9 - 2) * 96 / 2.54;
|
|
}
|
|
printableWidth = Math.round(printableWidth);
|
|
|
|
let wprint = window.open('');
|
|
wprint.document.title = 'pyRCV2 Report';
|
|
|
|
// Add stylesheets
|
|
for (let elCSSBase of document.querySelectorAll('head link')) {
|
|
let elCSS = wprint.document.createElement('link');
|
|
elCSS.rel = elCSSBase.rel;
|
|
elCSS.type = elCSSBase.type;
|
|
elCSS.href = elCSSBase.href;
|
|
wprint.document.head.appendChild(elCSS);
|
|
}
|
|
|
|
// Configure printing
|
|
let elStyle = wprint.document.createElement('style');
|
|
elStyle.innerHTML = '@page { size: ' + paperSize + ' landscape; margin: 1cm; } @media print { body { padding: 0; } }';
|
|
wprint.document.head.appendChild(elStyle);
|
|
|
|
let elContainer = wprint.document.createElement('div');
|
|
elContainer.id = 'printContainer';
|
|
elContainer.style.width = printableWidth + 'px';
|
|
wprint.document.body.appendChild(elContainer);
|
|
|
|
// Copy result logs 1
|
|
let divResultLogs1 = document.getElementById('resultLogs1');
|
|
let divResultLogs2 = wprint.document.createElement('div');
|
|
divResultLogs2.innerHTML = divResultLogs1.innerHTML;
|
|
elContainer.appendChild(divResultLogs2);
|
|
|
|
// Parse table, accounting for rowspan
|
|
let elTrs1 = document.querySelector('#result').rows;
|
|
let rows = [];
|
|
for (let elTr1 of elTrs1) {
|
|
rows.push([]);
|
|
}
|
|
for (let r = 0; r < elTrs1.length; r++) {
|
|
for (let c = 0; c < elTrs1[r].cells.length; c++) {
|
|
let elTd1 = elTrs1[r].cells[c];
|
|
rows[r].push(elTd1);
|
|
|
|
let rowspan = elTd1.getAttribute('rowspan');
|
|
// NB: Only works for rowspan in first column
|
|
if (rowspan !== null && c == 0) {
|
|
rowspan = parseInt(rowspan);
|
|
// Add ghost cells
|
|
for (let i = 1; i < rowspan; i++) {
|
|
rows[r + i].push(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyColumn(c, elTrs2) {
|
|
let tdsAdded = [];
|
|
for (let r = 0; r < rows.length; r++) {
|
|
if (c < rows[r].length) {
|
|
let elTd1 = rows[r][c];
|
|
if (elTd1 !== null) {
|
|
let elTd2 = wprint.document.createElement('td');
|
|
elTd2.innerHTML = elTd1.innerHTML;
|
|
elTd2.className = elTd1.className;
|
|
elTd2.setAttribute('rowspan', elTd1.getAttribute('rowspan'));
|
|
elTd2.setAttribute('style', elTd1.getAttribute('style'));
|
|
elTrs2[r].appendChild(elTd2);
|
|
tdsAdded.push(elTd2);
|
|
}
|
|
}
|
|
}
|
|
return tdsAdded;
|
|
}
|
|
|
|
function copyTableColumns(startCol) {
|
|
// Add table
|
|
let elTable2 = wprint.document.createElement('table');
|
|
elTable2.className = 'result';
|
|
if (startCol > 1) {
|
|
elTable2.style.pageBreakBefore = 'always';
|
|
}
|
|
elContainer.appendChild(elTable2);
|
|
|
|
// Add rows
|
|
let elTrs2 = [];
|
|
for (let elTr1 of elTrs1) {
|
|
let elTr2 = wprint.document.createElement('tr');
|
|
elTr2.className = elTr1.className;
|
|
elTrs2.push(elTr2);
|
|
elTable2.appendChild(elTr2);
|
|
}
|
|
|
|
// Copy first column
|
|
copyColumn(0, elTrs2);
|
|
|
|
// Copy column by column
|
|
for (let c = startCol; c < rows[0].length + 1; c++) { // +1 to account for final status column
|
|
let tdsAdded = copyColumn(c, elTrs2);
|
|
|
|
// Check if overflowed
|
|
if (elTable2.clientWidth > printableWidth) {
|
|
for (let elTd2 of tdsAdded) {
|
|
elTd2.remove();
|
|
}
|
|
// Start new table
|
|
return copyTableColumns(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
copyTableColumns(1);
|
|
|
|
// Copy result logs2
|
|
divResultLogs1 = document.getElementById('resultLogs2');
|
|
divResultLogs2 = wprint.document.createElement('div');
|
|
divResultLogs2.innerHTML = divResultLogs1.innerHTML;
|
|
elContainer.appendChild(divResultLogs2);
|
|
|
|
wprint.print();
|
|
}
|