Skip to content

Commit

Permalink
three-step gui tabulation
Browse files Browse the repository at this point in the history
  • Loading branch information
artoonie committed Apr 21, 2024
1 parent e38b70b commit 2c50723
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 76 deletions.
40 changes: 39 additions & 1 deletion src/main/java/network/brightspots/rcv/GuiConfigController.java
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,17 @@ public void menuItemTabulateClicked() {
openTabulateWindow();
}

/**
* Tabulate whatever is currently entered into the GUI.
* Assumes GuiTabulateController checked all prerequisites.
*/
public Service<Integer> parseAndCountCastVoteRecords(String configPath) {
setGuiIsBusy(true);
Service<Integer> service = new ReadCastVoteRecordsService(configPath);
setUpAndStartService(service);
return service;
}

/**
* Tabulate whatever is currently entered into the GUI.
* Assumes GuiTabulateController checked all prerequisites.
Expand Down Expand Up @@ -535,7 +546,7 @@ public void menuItemConvertToCdfClicked() {
}
}

private void setUpAndStartService(Service<Boolean> service) {
private <T> void setUpAndStartService(Service<T> service) {
service.setOnSucceeded(event -> setGuiIsBusy(false));
service.setOnCancelled(event -> setGuiIsBusy(false));
service.setOnFailed(event -> setGuiIsBusy(false));
Expand Down Expand Up @@ -1794,6 +1805,33 @@ protected Boolean call() {
}
}

// ReadCastVoteRecordsService reads CVRs
private static class ReadCastVoteRecordsService extends Service<Integer> {
private final String configPath;

ReadCastVoteRecordsService(String configPath) {
this.configPath = configPath;
}

@Override
protected Task<Integer> createTask() {
return new Task<>() {
@Override
protected Integer call() {
TabulatorSession session = new TabulatorSession(configPath);
int count = session.parseAndCountCastVoteRecords();
if (count >= 0) {
succeeded();
} else {
Logger.warning("There were errors");
failed();
}
return count;
}
};
}
}

private abstract static class EditableColumn {
protected final TableColumn column;
protected final String propertyName;
Expand Down
195 changes: 131 additions & 64 deletions src/main/java/network/brightspots/rcv/GuiTabulateController.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package network.brightspots.rcv;

import java.io.File;
import java.io.IOException;
import javafx.concurrent.Service;
import javafx.concurrent.WorkerStateEvent;
Expand All @@ -35,16 +36,6 @@
/** View controller for tabulator layout. */
@SuppressWarnings("WeakerAccess")
public class GuiTabulateController {
/**
* The button text to display before tabulation begins.
*/
private static final String buttonTabulateText = "Tabulate";

/**
* The button text to display while tabulation is in progress.
*/
private static final String buttonTabulateInProgressText = "Tabulating...";

/**
* The button text to display after tabulation successfully completes.
*/
Expand All @@ -57,13 +48,15 @@ public class GuiTabulateController {

/**
* Once the file is saved, cache it here. It cannot be changed while this modal is open.
* Do not rely on this variable alone -- it should be used in conjunction with
* useTemporaryConfigBeforeTabulation. It will be null if the Save Temp File button was used.
*/
private String savedConfigFilePath = null;

/**
* Cache whether the user is using a temporary config.
* Before user hits tabulate, save a temporary config and use it?
*/
private boolean isSavedConfigFileTemporary = false;
private boolean useTemporaryConfigBeforeTabulation = false;

/**
* The output folder of the tabulated config.
Expand All @@ -87,14 +80,22 @@ public class GuiTabulateController {
*/
private String unfilledFieldStyle;

/**
* Did the last task fail?
*/
private boolean lastTaskFailed = false;

@FXML private TextArea filepath;
@FXML private Button saveButton;
@FXML private Button tempSaveButton;
@FXML private Label numberOfCandidates;
@FXML private Label numberOfCvrs;
@FXML private Label numberOfCvrFiles;
@FXML private Label numberOfBallots;
@FXML private TextField userNameField;
@FXML private ProgressBar progressBar;
@FXML private Button loadCvrButton;
@FXML private Button tabulateButton;
@FXML private Button openResultsButton;
@FXML private Text progressText;

/**
Expand All @@ -103,7 +104,9 @@ public class GuiTabulateController {
public void initialize(GuiConfigController controller, int numCandidates, int numCvrs) {
guiConfigController = controller;
numberOfCandidates.setText("Number of candidates: " + numCandidates);
numberOfCvrs.setText("Number of CVRs: " + numCvrs);
numberOfCvrFiles.setText("Number of CVR Files: " + numCvrs);
numberOfBallots.setText("Number of Ballots: <load CVRs to count>");
numberOfBallots.setOpacity(0.5);
filledFieldStyle = "";
unfilledFieldStyle = "-fx-border-color: red;";

Expand All @@ -126,43 +129,85 @@ public void nameUpdated(KeyEvent keyEvent) {
updateProgressText();
}

/**
* Action when the Load CVRs button is clicked.
*
* @param actionEvent ignored
*/
public void buttonloadCvrsClicked(ActionEvent actionEvent) {
String configPath = getConfigPathOrCreateTempFile();
ContestConfig config = ContestConfig.loadContestConfig(configPath);
configOutputPath = config.getOutputDirectory();
if (!configOutputPath.endsWith("/")) {
configOutputPath += "/";
}

enableButtonsUpTo(null);
Service<Integer> service = guiConfigController.parseAndCountCastVoteRecords(configPath);
// Dispatch a function that watches the service and updates the progress bar
watchParseCVRServiceProgress(service);
}

/**
* Action when the tabulate button is clicked.
*
* @param actionEvent ignored
*/
public void buttonTabulateClicked(ActionEvent actionEvent) {
switch (tabulateButton.getText()) {
case buttonTabulateText:
ContestConfig config = ContestConfig.loadContestConfig(savedConfigFilePath);
configOutputPath = config.getOutputDirectory();
if (!configOutputPath.endsWith("/")) {
configOutputPath += "/";
}

Service<Boolean> service = guiConfigController.startTabulation(
savedConfigFilePath, userNameField.getText(), isSavedConfigFileTemporary);
// Dispatch a function that watches the service and updates the progress bar
watchServiceProgress(service);
break;
case buttonOpenResultsText:
openOutputDirectoryInFileExplorer();
break;
case buttonViewErrorLogsText:
// Close the window
Window window = tabulateButton.getScene().getWindow();
((Stage) window).close();
break;
default:
throw new IllegalStateException("Unexpected value: " + tabulateButton.getText());
String configPath = getConfigPathOrCreateTempFile();
ContestConfig config = ContestConfig.loadContestConfig(configPath);
configOutputPath = config.getOutputDirectory();
if (!configOutputPath.endsWith("/")) {
configOutputPath += "/";
}

Service<Boolean> service = guiConfigController.startTabulation(
configPath, userNameField.getText(), false);
// Dispatch a function that watches the service and updates the progress bar
watchTabulatorServiceProgress(service);
}

/**
* Action when Open Results button is clicked.
*
* @param actionEvent ignored
*/
public void buttonOpenResultsClicked(ActionEvent actionEvent) {
if (lastTaskFailed) {
openOutputDirectoryInFileExplorer();
} else {
// Close the window
Window window = tabulateButton.getScene().getWindow();
((Stage) window).close();
}
}

private void watchServiceProgress(Service<Boolean> service) {
private void watchTabulatorServiceProgress(Service<Boolean> service) {
EventHandler<WorkerStateEvent> onSuceededEvent = workerStateEvent -> {
lastTaskFailed = service.getValue();
if (lastTaskFailed) {
openResultsButton.setText(buttonOpenResultsText);
} else {
openResultsButton.setText(buttonViewErrorLogsText);
}
enableButtonsUpTo(openResultsButton);
};
watchGenericService(service, onSuceededEvent);
}

private void watchParseCVRServiceProgress(Service<Integer> service) {
EventHandler<WorkerStateEvent> onSuceededEvent = workerStateEvent -> {
enableButtonsUpTo(tabulateButton);
numberOfBallots.setText("Number of Ballots: " + service.getValue());
numberOfBallots.setOpacity(1);
};

watchGenericService(service, onSuceededEvent);
}

private <T> void watchGenericService(Service<T> service, EventHandler<WorkerStateEvent> onSuccessCallback) {
progressBar.progressProperty().bind(service.progressProperty());
tabulateButton.setText(buttonTabulateInProgressText);
tabulateButton.setDisable(true);
userNameField.setDisable(true);
enableButtonsUpTo(null);

// This is a litle hacky -- we want two listeners on setOnSucceded,
// so we enforce that this callback is added second.
Expand All @@ -173,29 +218,45 @@ private void watchServiceProgress(Service<Boolean> service) {
}

service.setOnSucceeded(workerStateEvent -> {
originalCallback.handle(workerStateEvent);
progressBar.progressProperty().unbind();
tabulateButton.setDisable(false);

boolean succeeded = service.getValue();
if (succeeded) {
progressBar.setProgress(1);
tabulateButton.setText(buttonOpenResultsText);
} else {
progressBar.setProgress(0);
tabulateButton.setText(buttonViewErrorLogsText);
}
progressBar.setProgress(1);
originalCallback.handle(workerStateEvent);
onSuccessCallback.handle(workerStateEvent);
});
}

/**
* In the list of three buttons, Read > Tabulate > Open, sets the buttons up
* until this button as enabled.
* @param button Last button to be enabled, or null to disable all buttons
*/
private void enableButtonsUpTo(Button button) {
loadCvrButton.setDisable(true);
tabulateButton.setDisable(true);
openResultsButton.setDisable(true);

if (button == loadCvrButton) {
loadCvrButton.setDisable(false);
} else if (button == tabulateButton) {
loadCvrButton.setDisable(false);
tabulateButton.setDisable(false);
} else if (button == openResultsButton) {
loadCvrButton.setDisable(false);
tabulateButton.setDisable(false);
openResultsButton.setDisable(false);
} else if (button != null) {
throw new IllegalArgumentException("Invalid button");
}
}

/**
* Action when the save button is clicked.
*
* @param actionEvent ignored
*/
public void buttonSaveClicked(ActionEvent actionEvent) {
savedConfigFilePath = guiConfigController.saveFile(saveButton, false);
if (savedConfigFilePath != null) {
if (isConfigSavedOrTempFileReady()) {
saveButton.setText("Save");
tempSaveButton.setText("Temp File Saved!");
filepath.setText(savedConfigFilePath);
Expand All @@ -210,31 +271,37 @@ public void buttonSaveClicked(ActionEvent actionEvent) {
* @param actionEvent ignored
*/
public void buttonTempSaveClicked(ActionEvent actionEvent) {
savedConfigFilePath = guiConfigController.saveFile(tempSaveButton, true);
isSavedConfigFileTemporary = true;
useTemporaryConfigBeforeTabulation = true;
saveButton.setText("Save");
tempSaveButton.setText("Saved!");
updateGuiNotifyConfigSaved();
setTabulationButtonStatus();
}

private String getConfigPathOrCreateTempFile() {
return useTemporaryConfigBeforeTabulation
? guiConfigController.saveFile(tempSaveButton, true)
: savedConfigFilePath;
}

private boolean isConfigSavedOrTempFileReady() {
return savedConfigFilePath != null || useTemporaryConfigBeforeTabulation;
}

private void setTabulationButtonStatus() {
if (savedConfigFilePath != null) {
if (isConfigSavedOrTempFileReady()) {
// Don't override the progress text unless we're past the Save stage
updateGuiWithNameEnteredStatus();
}

if (savedConfigFilePath != null && !userNameField.getText().isEmpty()) {
tabulateButton.setDisable(false);
if (isConfigSavedOrTempFileReady() && !userNameField.getText().isEmpty()) {
enableButtonsUpTo(loadCvrButton);
} else {
tabulateButton.setDisable(true);
enableButtonsUpTo(null);
}
}

private void updateGuiNotifyConfigSaved() {
if (savedConfigFilePath == null) {
throw new RuntimeException("There must be a saved file before calling this function.");
}

filepath.setStyle(filledFieldStyle);
tempSaveButton.setStyle(filledFieldStyle);
saveButton.setStyle(filledFieldStyle);
Expand Down Expand Up @@ -278,7 +345,7 @@ private void initializeSaveButtonStatuses() {
}

private void updateProgressText() {
if (savedConfigFilePath == null) {
if (!isConfigSavedOrTempFileReady()) {
progressText.setText("Save the config file to continue.");
} else if (userNameField.getText().isEmpty()) {
progressText.setText("Please enter your name to continue.");
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/network/brightspots/rcv/TabulatorSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ boolean convertToCdf() {
return conversionSuccess;
}

int parseAndCountCastVoteRecords() {
ContestConfig config = ContestConfig.loadContestConfig(configPath);
List<CastVoteRecord> cvrs = parseCastVoteRecords(config);
return cvrs == null ? -1 : cvrs.size();
}

// Returns a List of exception class names that were thrown while tabulating.
// Operator name is required for the audit logs.
// Note: An exception MUST be returned any time tabulation does not run.
Expand Down
Loading

0 comments on commit 2c50723

Please sign in to comment.