diff --git a/.gitignore b/.gitignore index 24f793a..eb4adb0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /.python-version /__target__ /test -/bundle.js +/html/bundle.js __pycache__ diff --git a/build.sh b/build.sh index 4d3d409..322c623 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,6 @@ #!/bin/env bash +OUTFILE=html/bundle.js + # Transcrypt transcrypt $@ --nomin pyRCV2.transcrypt || exit 1 @@ -10,7 +12,7 @@ perl -0777 -i -pe 's#export function sum \(iterable\) {\n let result = 0;#exp perl -0777 -i -pe 's#key \(a\) > key \(b\) \?#__gt__\(key \(a\), key \(b\)\) \?#g' __target__/org.transcrypt.__runtime__.js || exit 1 # Roll up -rollup __target__/pyRCV2.transcrypt.js -o bundle.js --name py -f iife --no-treeshake || exit 1 +rollup __target__/pyRCV2.transcrypt.js -o $OUTFILE --name py -f iife --no-treeshake || exit 1 -# Patch -perl -0777 -i -pe "s#'__index0__'#__index0__#g" bundle.js || exit 1 +# Patch incorrectly generated iterator functions +perl -0777 -i -pe "s#'__index0__'#__index0__#g" $OUTFILE || exit 1 diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..0efc28d --- /dev/null +++ b/html/index.html @@ -0,0 +1,122 @@ + + + + + pyRCV2 + + + +
+ + + + +
+ + + +
+ + + + + + + diff --git a/html/index.js b/html/index.js new file mode 100644 index 0000000..d982955 --- /dev/null +++ b/html/index.js @@ -0,0 +1,260 @@ +/* + 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 . +*/ + +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'; + } else if (document.getElementById('selPreset').value === 'prsa77') { + document.getElementById('selQuotaCriterion').value = 'geq'; + document.getElementById('selQuota').value = 'droop'; + document.getElementById('chkProgQuota').checked = false; + document.getElementById('chkBulkExclusion').checked = false; + document.getElementById('selNumbers').value = 'int'; + document.getElementById('selSurplus').value = 'order'; + document.getElementById('selTransfers').value = 'uig'; + document.getElementById('selTies').value = 'backwards_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 + + // 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); + + if (document.getElementById('selNumbers').value === 'int') { + tblResults.appendChild(elLTF1); + tblResults.appendChild(elLTF2); + } + + // 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'; + if (countCard.transfers != '0.00') { + elTd.innerText = countCard.transfers; + } + elTr1.appendChild(elTd); + + elTd = document.createElement('td'); + elTd.classList.add('count'); + if (countCard.state === py.pyRCV2.model.CandidateState.WITHDRAWN) { + elTd.classList.add('excluded'); + elTd.innerText = 'WD'; + } else if (countCard.state === py.pyRCV2.model.CandidateState.ELECTED || countCard.state === py.pyRCV2.model.CandidateState.PROVISIONALLY_ELECTED) { + elTd.classList.add('elected'); + elTd.innerText = countCard.votes; + + elTr1.querySelector('td:first-child').classList.add('elected'); + } else if (countCard.state === py.pyRCV2.model.CandidateState.EXCLUDED) { + elTd.classList.add('excluded'); + elTd.innerText = 'EX'; + } else { + elTd.innerText = countCard.votes; + } + elTr2.appendChild(elTd); + } + + // Display exhausted votes + elTd = document.createElement('td'); + elTd.classList.add('count'); + elTd.style.borderTop = '1px solid black'; + if (result.exhausted.transfers != '0.00') { + elTd.innerText = result.exhausted.transfers; + } + 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'; + if (result.loss_fraction.transfers != '0.00') { + elTd.innerText = result.loss_fraction.transfers; + } + elLTF1.appendChild(elTd); + + elTd = document.createElement('td'); + elTd.classList.add('count'); + elTd.innerText = result.loss_fraction.votes; + 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), + '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, + 'transfer_value': document.getElementById('selTransfers').value, + 'ties': document.getElementById('selTies').value + }, + 'data': text + }); +} diff --git a/worker.js b/html/worker.js similarity index 71% rename from worker.js rename to html/worker.js index 9f919f8..40a3337 100644 --- a/worker.js +++ b/html/worker.js @@ -1,10 +1,25 @@ +/* + 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 . +*/ + importScripts('http://peterolson.github.com/BigRational.js/BigInt_BigRat.min.js', 'https://cdn.jsdelivr.net/npm/big.js@6.0.0/big.min.js', 'bundle.js'); -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -onmessage = async function(evt) { +onmessage = function(evt) { + // Set settings if (evt.data.numbers === 'native') { py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Native); } @@ -16,6 +31,7 @@ onmessage = async function(evt) { } if (evt.data.numbers === 'fixed') { py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.Fixed); + py.pyRCV2.numbers.set_dps(evt.data.fixedDPs); } let election = py.pyRCV2.blt.readBLT(evt.data.data); @@ -24,7 +40,7 @@ onmessage = async function(evt) { }}); // Create counter - let counter = py.pyRCV2.method.base_stv.BaseSTVCounter(election); + let counter = py.pyRCV2.method.base_stv.BaseSTVCounter(election, evt.data.options); // Reset let result = counter.reset(); @@ -50,7 +66,6 @@ onmessage = async function(evt) { // Step election while (true) { - //await sleep(1000); result = counter.step(); if (py.isinstance(result, py.pyRCV2.model.CountCompleted)) { postMessage({'type': 'done'}); diff --git a/test.html b/test.html deleted file mode 100644 index b417c59..0000000 --- a/test.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - pyRCV2 test - - - - - - - -
- - - - - - - -