2020-10-18 18:58:24 +11:00
|
|
|
/*
|
|
|
|
pyRCV2: Preferential vote counting
|
|
|
|
Copyright © 2020 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 = 'block';
|
|
|
|
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('chkProgQuota').checked = false;
|
|
|
|
document.getElementById('chkBulkExclusion').checked = false;
|
|
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
|
|
document.getElementById('txtDP').value = '5';
|
|
|
|
document.getElementById('selSurplus').value = 'size';
|
|
|
|
document.getElementById('selTransfers').value = 'wig';
|
|
|
|
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('chkProgQuota').checked = true;
|
|
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
|
|
document.getElementById('selNumbers').value = 'rational';
|
|
|
|
document.getElementById('selSurplus').value = 'size';
|
|
|
|
document.getElementById('selTransfers').value = 'wig';
|
|
|
|
document.getElementById('selTies').value = 'backwards_random';
|
2020-10-18 20:41:48 +11:00
|
|
|
} else if (document.getElementById('selPreset').value === 'senate') {
|
2020-10-18 18:58:24 +11:00
|
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
|
|
document.getElementById('selQuota').value = 'droop';
|
|
|
|
document.getElementById('chkProgQuota').checked = false;
|
2020-10-18 21:47:59 +11:00
|
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
2020-10-18 18:58:24 +11:00
|
|
|
document.getElementById('selNumbers').value = 'int';
|
|
|
|
document.getElementById('selSurplus').value = 'order';
|
|
|
|
document.getElementById('selTransfers').value = 'uig';
|
|
|
|
document.getElementById('selTies').value = 'backwards_random';
|
2020-10-18 21:47:59 +11:00
|
|
|
} else if (document.getElementById('selPreset').value === 'wright') {
|
|
|
|
document.getElementById('selQuotaCriterion').value = 'geq';
|
|
|
|
document.getElementById('selQuota').value = 'droop';
|
|
|
|
document.getElementById('chkProgQuota').checked = false;
|
|
|
|
document.getElementById('chkBulkExclusion').checked = true;
|
|
|
|
document.getElementById('selNumbers').value = 'fixed';
|
|
|
|
document.getElementById('txtDP').value = '5';
|
|
|
|
document.getElementById('selSurplus').value = 'size';
|
|
|
|
document.getElementById('selTransfers').value = 'wright';
|
|
|
|
document.getElementById('selTies').value = 'backwards_random';
|
2020-10-18 18:58:24 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
// Step election
|
|
|
|
let worker = new Worker('worker.js');
|
|
|
|
let election, elComment, elExhausted1, elExhausted2, elLTF1, elLTF2, elTotal, elQuota;
|
|
|
|
|
|
|
|
worker.onmessage = function(evt) {
|
|
|
|
if (evt.data.type === 'init') {
|
|
|
|
election = evt.data.election;
|
|
|
|
|
|
|
|
// Comment row
|
|
|
|
elComment = document.createElement('tr');
|
|
|
|
let elTd = document.createElement('td');
|
|
|
|
elComment.appendChild(elTd);
|
|
|
|
tblResults.appendChild(elComment);
|
|
|
|
|
|
|
|
// 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
|
|
|
|
elExhausted1 = document.createElement('tr');
|
|
|
|
elExhausted1.classList.add('info');
|
|
|
|
elExhausted2 = document.createElement('tr');
|
|
|
|
elExhausted2.classList.add('info');
|
|
|
|
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.setAttribute('rowspan', '2');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
|
|
|
elTd.innerText = 'Exhausted';
|
|
|
|
elExhausted1.appendChild(elTd);
|
|
|
|
|
|
|
|
tblResults.appendChild(elExhausted1);
|
|
|
|
tblResults.appendChild(elExhausted2);
|
|
|
|
|
|
|
|
// Loss to fraction row
|
|
|
|
elLTF1 = document.createElement('tr');
|
|
|
|
elLTF1.classList.add('info');
|
|
|
|
elLTF2 = document.createElement('tr');
|
|
|
|
elLTF2.classList.add('info');
|
|
|
|
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.setAttribute('rowspan', '2');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
|
|
|
elTd.innerText = 'Loss to fraction';
|
|
|
|
elLTF1.appendChild(elTd);
|
|
|
|
|
2020-10-18 20:41:48 +11:00
|
|
|
tblResults.appendChild(elLTF1);
|
|
|
|
tblResults.appendChild(elLTF2);
|
2020-10-18 18:58:24 +11:00
|
|
|
|
|
|
|
// Total row
|
|
|
|
elTotal = document.createElement('tr');
|
|
|
|
elTotal.classList.add('info');
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
|
|
|
elTd.innerText = 'Total';
|
|
|
|
elTotal.appendChild(elTd);
|
|
|
|
tblResults.appendChild(elTotal);
|
|
|
|
|
|
|
|
// Quota row
|
|
|
|
elQuota = document.createElement('tr');
|
|
|
|
elQuota.classList.add('info');
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
|
|
|
elTd.style.borderBottom = '1px solid black';
|
|
|
|
elTd.innerText = 'Quota';
|
|
|
|
elQuota.appendChild(elTd);
|
|
|
|
tblResults.appendChild(elQuota);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (evt.data.type === 'result') {
|
|
|
|
let result = evt.data.result;
|
|
|
|
|
|
|
|
// Display results
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.innerText = result.comment;
|
|
|
|
elComment.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) {
|
|
|
|
elTd.classList.add('excluded');
|
|
|
|
} else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
|
|
|
elTd.classList.add('elected');
|
|
|
|
}
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
2020-10-18 20:41:48 +11:00
|
|
|
if (countCard.transfers != '0.00' && countCard.transfers != '0') {
|
2020-10-18 18:58:24 +11:00
|
|
|
elTd.innerText = countCard.transfers;
|
2020-12-23 19:19:11 +11:00
|
|
|
} else {
|
|
|
|
elTd.innerHTML = ' ';
|
2020-10-18 18:58:24 +11:00
|
|
|
}
|
|
|
|
elTr1.appendChild(elTd);
|
|
|
|
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
2020-10-19 01:39:53 +11:00
|
|
|
|
|
|
|
if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) {
|
2020-10-18 18:58:24 +11:00
|
|
|
elTd.classList.add('elected');
|
|
|
|
elTd.innerText = countCard.votes;
|
|
|
|
elTr1.querySelector('td:first-child').classList.add('elected');
|
|
|
|
} else {
|
2020-10-19 01:39:53 +11:00
|
|
|
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');
|
2020-12-23 19:19:11 +11:00
|
|
|
elTd.innerText = 'Ex';
|
2020-10-19 01:39:53 +11:00
|
|
|
} else {
|
|
|
|
elTd.innerText = countCard.votes;
|
|
|
|
}
|
2020-10-18 18:58:24 +11:00
|
|
|
}
|
2020-10-19 01:39:53 +11:00
|
|
|
|
2020-10-18 18:58:24 +11:00
|
|
|
elTr2.appendChild(elTd);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Display exhausted votes
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
2020-10-18 20:41:48 +11:00
|
|
|
if (result.exhausted.transfers != '0.00' && result.exhausted.transfers != '0') {
|
2020-10-18 18:58:24 +11:00
|
|
|
elTd.innerText = result.exhausted.transfers;
|
2020-12-23 19:19:11 +11:00
|
|
|
} else {
|
|
|
|
elTd.innerHTML = ' ';
|
2020-10-18 18:58:24 +11:00
|
|
|
}
|
|
|
|
elExhausted1.appendChild(elTd);
|
|
|
|
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
|
|
|
elTd.innerText = result.exhausted.votes;
|
|
|
|
elExhausted2.appendChild(elTd);
|
|
|
|
|
|
|
|
// Display loss to fraction
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
2020-10-18 20:41:48 +11:00
|
|
|
if (result.loss_fraction.transfers != '0.00' && result.loss_fraction.transfers != '-0.00' && result.loss_fraction.transfers != '0') {
|
2020-10-18 18:58:24 +11:00
|
|
|
elTd.innerText = result.loss_fraction.transfers;
|
2020-12-23 19:19:11 +11:00
|
|
|
} else {
|
|
|
|
elTd.innerHTML = ' ';
|
2020-10-18 18:58:24 +11:00
|
|
|
}
|
|
|
|
elLTF1.appendChild(elTd);
|
|
|
|
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
2020-10-18 20:41:48 +11:00
|
|
|
if (result.loss_fraction.votes == '-0.00') {
|
|
|
|
elTd.innerText = '0.00';
|
|
|
|
} else {
|
|
|
|
elTd.innerText = result.loss_fraction.votes;
|
|
|
|
}
|
2020-10-18 18:58:24 +11:00
|
|
|
elLTF2.appendChild(elTd);
|
|
|
|
|
|
|
|
// Display total
|
|
|
|
elTd = document.createElement('td');
|
|
|
|
elTd.classList.add('count');
|
|
|
|
elTd.style.borderTop = '1px solid black';
|
|
|
|
elTd.innerText = result.total;
|
|
|
|
elTotal.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.innerText = result.quota;
|
|
|
|
elQuota.appendChild(elTd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
worker.onerror = function(evt) {
|
|
|
|
throw evt;
|
|
|
|
}
|
|
|
|
|
|
|
|
worker.postMessage({
|
|
|
|
'numbers': document.getElementById('selNumbers').value,
|
|
|
|
'fixedDPs': parseInt(document.getElementById('txtDP').value),
|
2020-10-18 20:41:48 +11:00
|
|
|
'transfers': document.getElementById('selTransfers').value,
|
2020-10-18 18:58:24 +11:00
|
|
|
'options': {
|
|
|
|
'quota_criterion': document.getElementById('selQuotaCriterion').value,
|
|
|
|
'quota': document.getElementById('selQuota').value,
|
|
|
|
'prog_quota': document.getElementById('chkProgQuota').checked,
|
|
|
|
'bulk_exclude': document.getElementById('chkBulkExclusion').checked,
|
|
|
|
'surplus_order': document.getElementById('selSurplus').value,
|
|
|
|
'ties': document.getElementById('selTies').value
|
|
|
|
},
|
2020-12-24 00:04:30 +11:00
|
|
|
'seed': document.getElementById('txtSeed').value,
|
2020-10-18 18:58:24 +11:00
|
|
|
'data': text
|
|
|
|
});
|
|
|
|
}
|
2020-12-24 00:04:30 +11:00
|
|
|
|
|
|
|
// 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());
|
|
|
|
}
|