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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-