diff --git a/Cargo.lock b/Cargo.lock
index cb324ce..8dcd747 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -517,6 +517,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "html-escape"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "816ea801a95538fc5f53c836697b3f8b64a9d664c4f0b91efe1fe7c92e4dbcb7"
+dependencies = [
+ "utf8-width",
+]
+
[[package]]
name = "ibig"
version = "0.3.2"
@@ -637,9 +646,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-bigint"
-version = "0.4.0"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
+checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
dependencies = [
"autocfg",
"num-integer",
@@ -710,6 +719,7 @@ dependencies = [
"derive_more",
"flate2",
"git-version",
+ "html-escape",
"ibig",
"itertools",
"js-sys",
@@ -1124,6 +1134,12 @@ dependencies = [
"arrayvec",
]
+[[package]]
+name = "utf8-width"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b"
+
[[package]]
name = "vec_map"
version = "0.8.2"
diff --git a/Cargo.toml b/Cargo.toml
index c22cbea..043fc76 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,15 +14,16 @@ git-version = "0.3.4"
ibig = "0.3.2"
itertools = "0.10.1"
ndarray = "0.15.3"
-predicates = "1.0.8"
num-traits = "0.2"
+predicates = "1.0.8"
sha2 = "0.9.5"
-wasm-bindgen = "0.2.74"
+wasm-bindgen = "=0.2.74" # 0.2.77 causes "remaining data" error
# Only for WebAssembly - include here for syntax highlighting
#[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
js-sys = "0.3.51"
+html-escape = "0.2.9"
num-bigint = "0.4.0"
num-rational = "0.4.0"
paste = "1.0.5"
diff --git a/html/index.js b/html/index.js
index a4e17e6..ad39b40 100644
--- a/html/index.js
+++ b/html/index.js
@@ -29,6 +29,8 @@ var tblResult = document.getElementById('result');
var divLogs2 = document.getElementById('resultLogs2');
var olStageComments;
+var detailedTransfers = {};
+
var worker = new Worker('worker.js?v=GITVERSION');
worker.onmessage = function(evt) {
@@ -78,10 +80,13 @@ worker.onmessage = function(evt) {
} else if (evt.data.type === 'updateStageComments') {
let elLi = document.createElement('li');
- elLi.id = 'stage' + (olStageComments.childElementCount + 1);
+ elLi.id = 'stage' + evt.data.stageNum;
elLi.innerHTML = evt.data.comment;
olStageComments.append(elLi);
+ } else if (evt.data.type === 'updateDetailedTransfers') {
+ detailedTransfers[evt.data.stageNum] = evt.data.table;
+
} else if (evt.data.type === 'finalResultSummary') {
divLogs2.insertAdjacentHTML('beforeend', evt.data.summary);
document.getElementById('printPane').style.display = 'block';
@@ -170,6 +175,8 @@ async function clickCount() {
tblResult.innerHTML = '';
divLogs2.innerHTML = '';
+ detailedTransfers = {};
+
// Dispatch to worker
worker.postMessage({
'type': 'countElection',
@@ -187,6 +194,26 @@ async function clickCount() {
});
}
+function viewDetailedTransfers(stageNum) {
+ let wtransfers = window.open('', '', 'location=0,width=800,height=600');
+ wtransfers.document.title = 'OpenTally Detailed Transfers: Stage ' + stageNum;
+
+ // Add stylesheets
+ for (let elCSSBase of document.querySelectorAll('head link')) {
+ let elCSS = wtransfers.document.createElement('link');
+ elCSS.rel = elCSSBase.rel;
+ elCSS.type = elCSSBase.type;
+ if (elCSSBase.href.endsWith('?v=GITVERSION')) {
+ elCSS.href = elCSSBase.href.replace('?v=GITVERSION', '?v=' + Math.random());
+ } else {
+ elCSS.href = elCSSBase.href;
+ }
+ wtransfers.document.head.appendChild(elCSS);
+ }
+
+ wtransfers.document.body.innerHTML = detailedTransfers[stageNum];
+}
+
// Provide a default seed
if (document.getElementById('txtSeed').value === '') {
function pad(x) { if (x < 10) { return '0' + x; } return '' + x; }
diff --git a/html/main.css b/html/main.css
index 8acc577..b8e0914 100644
--- a/html/main.css
+++ b/html/main.css
@@ -94,7 +94,7 @@ table {
color-adjust: exact;
-webkit-print-color-adjust: exact;
}
-.result td {
+table.result td, table.transfers td {
padding: 0px 8px;
height: 1em;
}
@@ -127,10 +127,12 @@ td.elected {
tr.info td {
background-color: #f0f5fb;
}
-tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td {
+tr.stage-no td:not(:empty), tr.hint-papers-votes td:not(:empty), tr.transfers td,
+table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
border-top: 1px solid #76858c;
}
-tr.info:last-child td, .bb {
+tr.info:last-child td, .bb,
+table.transfers tr:first-child td, table.transfers tr:nth-last-child(2) td, table.transfers tr:last-child td {
border-bottom: 1px solid #76858c;
}
.blw {
@@ -138,13 +140,18 @@ tr.info:last-child td, .bb {
border-left: 2px solid #76858c;
}
+table.transfers tr:first-child td {
+ font-weight: 600;
+}
+
/* Table stripes */
tr.stage-no td:nth-child(even):not([rowspan]),
tr.stage-comment td:nth-child(odd),
tr.hint-papers-votes td:nth-child(even),
tr.candidate.transfers td:nth-child(even):not(.elected):not(.excluded),
-tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded) {
+tr.candidate.votes td:nth-child(odd):not(.elected):not(.excluded),
+table.transfers td:nth-child(even) {
background-color: #f9f9f9;
}
tr.candidate.transfers td.elected:nth-child(even),
@@ -161,33 +168,8 @@ tr.info.votes td:nth-child(odd) {
background-color: #e8eef7;
}
-/* BLT input tool */
-
-#selBallots {
- min-width: 10em;
- margin-right: 1em;
-}
-
-#bltMain {
- display: flex;
-}
-
-#tblBallot {
- margin-top: 0.5em;
- margin-bottom: 0.5em;
-}
-
-#tblBallot input {
- margin-right: 0.5ex;
-}
-
-#divEditCandidates div {
- margin-bottom: 0.5em;
-}
-
-#txtCandidates {
- min-width: 20em;
- min-height: 10em;
+a.detailedTransfersLink {
+ color: #aaa;
}
/* Print stylesheet */
diff --git a/html/worker.js b/html/worker.js
index f469684..95a96eb 100644
--- a/html/worker.js
+++ b/html/worker.js
@@ -105,7 +105,12 @@ function resumeCount() {
}
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts, reportStyle)});
- postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state)});
+ postMessage({'type': 'updateStageComments', 'comment': wasm['update_stage_comments_' + numbers](state, stageNum), 'stageNum': stageNum});
+
+ let transfers_table = state.transfer_table_render_html(opts);
+ if (transfers_table) {
+ postMessage({'type': 'updateDetailedTransfers', 'table': transfers_table, 'stageNum': stageNum});
+ }
}
postMessage({'type': 'updateResultsTable', 'result': wasm['finalise_results_table_' + numbers](state, reportStyle)});
diff --git a/src/stv/gregory/mod.rs b/src/stv/gregory/mod.rs
index 47ffa35..46724df 100644
--- a/src/stv/gregory/mod.rs
+++ b/src/stv/gregory/mod.rs
@@ -15,9 +15,19 @@
* along with this program. If not, see .
*/
+// --------------
+// Child packages
+
+/// Transfer tables
mod transfers;
pub use transfers::{TransferTable, TransferTableCell, TransferTableColumn};
+/// prettytable-compatible API for HTML table output in WebAssembly
+pub mod prettytable_html;
+
+// --------
+// STV code
+
use super::{ExclusionMethod, STVError, STVOptions, SurplusMethod, SurplusOrder};
use super::sample;
diff --git a/src/stv/gregory/prettytable_html.rs b/src/stv/gregory/prettytable_html.rs
new file mode 100644
index 0000000..14271e4
--- /dev/null
+++ b/src/stv/gregory/prettytable_html.rs
@@ -0,0 +1,107 @@
+/* OpenTally: Open-source election vote counting
+ * Copyright © 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 .
+ */
+
+use itertools::Itertools;
+
+/// Table
+pub struct Table {
+ /// Rows in the table
+ rows: Vec,
+}
+
+impl Table {
+ /// Return a new [Table]
+ pub fn new() -> Self {
+ Self {
+ rows: Vec::new(),
+ }
+ }
+
+ /// Add a [Row] to the table
+ pub fn add_row(&mut self, row: Row) {
+ self.rows.push(row);
+ }
+
+ /// Alias for [add_row]
+ pub fn set_titles(&mut self, row: Row) {
+ self.add_row(row);
+ }
+
+ /// Render the table as HTML
+ pub fn to_string(&self) -> String {
+ return format!(r#""#, self.rows.iter().map(|r| r.to_string()).join(""));
+ }
+}
+
+/// Row in a [Table]
+pub struct Row {
+ /// Cells in the row
+ cells: Vec,
+}
+
+impl Row {
+ /// Return a new [Row]
+ pub fn new(cells: Vec) -> Self {
+ Self {
+ cells
+ }
+ }
+
+ /// Render the row as HTML
+ fn to_string(&self) -> String {
+ return format!(r#"{} "#, self.cells.iter().map(|c| c.to_string()).join(""));
+ }
+}
+
+/// Cell in a [Row]
+pub struct Cell {
+ /// Content of the cell
+ content: String,
+ /// HTML tag/attributes
+ attrs: Vec<&'static str>,
+}
+
+impl Cell {
+ /// Return a new [Cell]
+ pub fn new(content: &str) -> Self {
+ Self {
+ content: String::from(content),
+ attrs: vec!["td"],
+ }
+ }
+
+ /// Apply a style to the cell
+ #[allow(unused_mut)]
+ pub fn style_spec(mut self, spec: &str) -> Self {
+ if spec.contains("H2") {
+ self.attrs.push(r#"colspan="2""#);
+ }
+ if spec.contains("c") {
+ self.attrs.push(r#"style="text-align:center""#);
+ }
+ if spec.contains("r") {
+ self.attrs.push(r#"style="text-align:right""#);
+ }
+
+ return self;
+ }
+
+ /// Render the cell as HTML
+ fn to_string(&self) -> String {
+ return format!(r#"<{}>{}"#, self.attrs.join(" "), html_escape::encode_text(&self.content));
+ }
+}
diff --git a/src/stv/gregory/transfers.rs b/src/stv/gregory/transfers.rs
index ed2240e..2ce1446 100644
--- a/src/stv/gregory/transfers.rs
+++ b/src/stv/gregory/transfers.rs
@@ -15,7 +15,10 @@
* along with this program. If not, see .
*/
+#[cfg(not(target_arch = "wasm32"))]
use prettytable::{Cell, Row, Table};
+#[cfg(target_arch = "wasm32")]
+use super::prettytable_html::{Cell, Row, Table};
use crate::election::{Candidate, CountState};
use crate::numbers::Number;
@@ -287,10 +290,10 @@ impl<'e, N: Number> TransferTable<'e, N> {
return checksum;
}
- /// Render table as plain text
- pub fn render_text(&self, state: &CountState, opts: &STVOptions) -> String {
+ /// Render table as [Table]
+ fn render(&self, state: &CountState, opts: &STVOptions) -> Table {
let mut table = Table::new();
- table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
+ set_table_format(&mut table);
let show_transfers_per_ballot = !self.surpfrac.is_none() && opts.sum_surplus_transfers == SumSurplusTransfersMode::PerBallot;
@@ -448,12 +451,18 @@ impl<'e, N: Number> TransferTable<'e, N> {
table.add_row(Row::new(row));
- return table.to_string();
+ return table;
+ }
+
+ /// Render table as plain text
+ #[cfg(not(target_arch = "wasm32"))]
+ pub fn render_text(&self, state: &CountState, opts: &STVOptions) -> String {
+ return self.render(state, opts).to_string();
}
/// Render table as HTML
- pub fn render_html(&self) -> String {
- todo!();
+ pub fn render_html(&self, state: &CountState, opts: &STVOptions) -> String {
+ return self.render(state, opts).to_string();
}
}
@@ -497,3 +506,13 @@ pub struct TransferTableCell {
/// Votes transferred to the continuing candidate
pub votes_out: N,
}
+
+#[cfg(not(target_arch = "wasm32"))]
+fn set_table_format(table: &mut Table) {
+ table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
+}
+
+#[cfg(target_arch = "wasm32")]
+fn set_table_format(_table: &mut Table) {
+ // No op
+}
diff --git a/src/stv/wasm.rs b/src/stv/wasm.rs
index 8d182b2..a5a3814 100644
--- a/src/stv/wasm.rs
+++ b/src/stv/wasm.rs
@@ -141,8 +141,8 @@ macro_rules! impl_type {
/// Wrapper for [update_stage_comments]
#[wasm_bindgen]
#[allow(non_snake_case)]
- pub fn [](state: &[]) -> String {
- return update_stage_comments(&state.0);
+ pub fn [](state: &[], stage_num: usize) -> String {
+ return update_stage_comments(&state.0, stage_num);
}
/// Wrapper for [finalise_results_table]
@@ -173,6 +173,14 @@ macro_rules! impl_type {
pub fn new(election: &[]) -> Self {
return [](CountState::new(election.as_static()));
}
+
+ /// Call [render_html](crate::stv::transfers::TransferTable::render_html) on [CountState::transfer_table]
+ pub fn transfer_table_render_html(&self, opts: &STVOptions) -> Option {
+ return match &self.0.transfer_table {
+ Some(tt) => Some(tt.render_html(&self.0, &opts.0)), // TODO
+ None => None,
+ };
+ }
}
/// Wrapper for [Election]
@@ -613,8 +621,12 @@ fn update_results_table(stage_num: usize, state: &CountState, opts
}
/// Get the comment for the current stage
-fn update_stage_comments(state: &CountState) -> String {
- return state.logger.render().join(" ");
+fn update_stage_comments(state: &CountState, stage_num: usize) -> String {
+ let mut comments = state.logger.render().join(" ");
+ if let Some(_) = state.transfer_table {
+ comments.push_str(&format!(r##" [View detailed transfers]"##, stage_num));
+ }
+ return comments;
}
/// Generate the final column of the HTML results table
| |