273 lines
7.1 KiB
JavaScript
273 lines
7.1 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/>.
|
|
*/
|
|
|
|
let candidates = [];
|
|
let ballots = [];
|
|
|
|
let inpFile = document.getElementById('inpFile');
|
|
let txtBallotValue = document.getElementById('txtBallotValue');
|
|
let tblBallot = document.getElementById('tblBallot');
|
|
let selBallots = document.getElementById('selBallots');
|
|
|
|
selBallots.value = 'new';
|
|
|
|
function initBallot() {
|
|
// Init ballot table
|
|
tblBallot.innerHTML = '';
|
|
for (let candidate of candidates) {
|
|
let elTr = document.createElement('tr');
|
|
|
|
let elTd = document.createElement('td');
|
|
let elInput = document.createElement('input');
|
|
elInput.type = 'number';
|
|
elInput.min = '1';
|
|
elInput.style.width = '3em';
|
|
elTd.appendChild(elInput);
|
|
elTr.appendChild(elTd);
|
|
|
|
// Add listener
|
|
elInput.addEventListener('keyup', function(evt) {
|
|
if (evt.key === 'Enter') {
|
|
evt.preventDefault();
|
|
clickSave();
|
|
}
|
|
});
|
|
|
|
elTd = document.createElement('td');
|
|
elTd.innerText = candidate;
|
|
elTr.appendChild(elTd);
|
|
|
|
tblBallot.appendChild(elTr);
|
|
}
|
|
}
|
|
|
|
function clickSave() {
|
|
// Save ballot
|
|
let ballot = {'value': txtBallotValue.value, 'preferences': []};
|
|
for (let elInput of tblBallot.querySelectorAll('input')) {
|
|
ballot['preferences'].push(elInput.value);
|
|
}
|
|
|
|
if (selBallots.value === 'new') {
|
|
// New ballot
|
|
ballots.push(ballot);
|
|
|
|
// Add new ballot <option>
|
|
let elOption = document.createElement('option');
|
|
elOption.innerText = 'Ballot ' + ballots.length;
|
|
selBallots.insertBefore(elOption, selBallots.selectedOptions[0]);
|
|
} else if (selBallots.selectedIndex >= 0) {
|
|
// Editing existing ballot
|
|
ballots[selBallots.selectedIndex] = ballot;
|
|
|
|
// Advance to next ballot
|
|
selBallots.selectedIndex += 1;
|
|
}
|
|
|
|
// Update ballot entry
|
|
changeBallot();
|
|
}
|
|
|
|
function changeBallot() {
|
|
if (selBallots.value === 'new') {
|
|
// Clear input
|
|
for (let elInput of tblBallot.querySelectorAll('input')) {
|
|
elInput.value = '';
|
|
}
|
|
txtBallotValue.value = '1';
|
|
} else if (selBallots.selectedIndex >= 0) {
|
|
// Update input
|
|
let ballot = ballots[selBallots.selectedIndex];
|
|
let i = 0;
|
|
for (let elInput of tblBallot.querySelectorAll('input')) {
|
|
elInput.value = ballot['preferences'][i];
|
|
i += 1;
|
|
}
|
|
txtBallotValue.value = ballot['value'];
|
|
}
|
|
|
|
// Move cursor to first input box
|
|
tblBallot.querySelector('input').focus();
|
|
tblBallot.querySelector('input').select();
|
|
}
|
|
|
|
let openMode = 'json';
|
|
|
|
function clickOpenJSON() {
|
|
openMode = 'json';
|
|
inpFile.value = '';
|
|
inpFile.click();
|
|
}
|
|
|
|
function clickImportBLT() {
|
|
openMode = 'blt';
|
|
inpFile.value = '';
|
|
inpFile.click();
|
|
}
|
|
|
|
async function changeInpFile() {
|
|
if (inpFile.value !== '') {
|
|
if (openMode === 'json') {
|
|
// Open JSON file
|
|
let text = await inpFile.files[0].text();
|
|
let obj = JSON.parse(text);
|
|
|
|
// Load data
|
|
candidates = obj['candidates'];
|
|
ballots = obj['ballots'];
|
|
} else if (openMode === 'blt') {
|
|
// Open BLT file
|
|
let text = await inpFile.files[0].text();
|
|
|
|
// Import data
|
|
py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.StringNum);
|
|
let election = py.pyRCV2.blt.readBLT(text);
|
|
|
|
candidates = [];
|
|
for (let candidate of election.candidates) {
|
|
candidates.push(candidate.py_name);
|
|
}
|
|
|
|
ballots = [];
|
|
for (let ballot of election.ballots) {
|
|
let js_ballot = {'value': ballot.value.__str__(), 'preferences': []};
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
js_ballot['preferences'].push('');
|
|
}
|
|
|
|
// Add preference numbers
|
|
for (let i = 0; i < ballot.preferences.length; i++) {
|
|
candidate = ballot.preferences[i];
|
|
js_ballot['preferences'][election.candidates.indexOf(candidate)] = '' + (i + 1);
|
|
}
|
|
|
|
ballots.push(js_ballot);
|
|
}
|
|
}
|
|
|
|
// Go to ballot entry screen
|
|
document.getElementById('bltMain').style.display = '';
|
|
document.getElementById('divEditCandidates').style.display = 'none';
|
|
|
|
// Update ballot entry
|
|
initBallot();
|
|
|
|
// Update ballot list
|
|
selBallots.innerHTML = '<option value="new" selected>-- New Ballot --</option>';
|
|
selBallots.value = 'new';
|
|
|
|
for (let i = 0; i < ballots.length; i++) {
|
|
let elOption = document.createElement('option');
|
|
elOption.innerText = 'Ballot ' + (i + 1);
|
|
selBallots.insertBefore(elOption, selBallots.selectedOptions[0]);
|
|
}
|
|
|
|
inpFile.value = '';
|
|
}
|
|
}
|
|
|
|
function clickSaveJSON() {
|
|
let result = {
|
|
'candidates': candidates,
|
|
'ballots': ballots,
|
|
};
|
|
|
|
// Download file
|
|
let url = URL.createObjectURL(new File([JSON.stringify(result)], 'ballots.json', {type: 'application/json'}));
|
|
let elA = document.createElement('a');
|
|
elA.href = url;
|
|
elA.download = '';
|
|
elA.click();
|
|
}
|
|
|
|
function clickExportBLT() {
|
|
let election = py.pyRCV2.model.Election();
|
|
|
|
let result = window.prompt('Enter the number of seats to elect:', '1');
|
|
if (result !== null) {
|
|
election.seats = parseInt(result);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
result = window.prompt('Enter the name of the election:');
|
|
if (result !== null) {
|
|
election.py_name = result;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
for (let candidate of candidates) {
|
|
let py_candidate = py.pyRCV2.model.Candidate(candidate);
|
|
election.candidates.push(py_candidate);
|
|
}
|
|
|
|
// Process ballots
|
|
for (let ballot of ballots) {
|
|
let py_ballot = py.pyRCV2.model.Ballot(py.pyRCV2.numbers.StringNum(ballot['value']), []);
|
|
|
|
for (let i = 1; i < candidates.length + 1; i++) {
|
|
let candidate = null;
|
|
for (let j = 0; j < candidates.length; j++) {
|
|
if (ballot.preferences[j].trim() === '' + i) {
|
|
candidate = j;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (candidate !== null) {
|
|
py_ballot.preferences.push(election.candidates[candidate]);
|
|
} else {
|
|
// No such preference
|
|
break;
|
|
}
|
|
}
|
|
|
|
election.ballots.push(py_ballot);
|
|
}
|
|
|
|
// Download file
|
|
let url = URL.createObjectURL(new File([py.pyRCV2.blt.writeBLT(election)], 'election.blt', {type: 'application/json'}));
|
|
let elA = document.createElement('a');
|
|
elA.href = url;
|
|
elA.download = '';
|
|
elA.click();
|
|
}
|
|
|
|
function clickEditCandidates() {
|
|
document.getElementById('bltMain').style.display = 'none';
|
|
document.getElementById('divEditCandidates').style.display = '';
|
|
document.getElementById('txtCandidates').value = candidates.join('\n');
|
|
}
|
|
|
|
function clickSaveCandidates() {
|
|
candidates = [];
|
|
for (let candidate of document.getElementById('txtCandidates').value.split('\n')) {
|
|
if (candidate.trim() !== '') {
|
|
candidates.push(candidate.trim());
|
|
}
|
|
}
|
|
|
|
// Update ballot entry
|
|
initBallot();
|
|
changeBallot();
|
|
|
|
document.getElementById('bltMain').style.display = '';
|
|
document.getElementById('divEditCandidates').style.display = 'none';
|
|
}
|