Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sundry UX fixes #509

Merged
merged 7 commits into from
Sep 22, 2020
23 changes: 14 additions & 9 deletions src/main/java/network/brightspots/rcv/ContestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@ class ContestConfig {
static final boolean SUGGESTED_CONTINUE_UNTIL_TWO_CANDIDATES_REMAIN = false;
static final boolean SUGGESTED_EXHAUST_ON_DUPLICATE_CANDIDATES = false;
static final boolean SUGGESTED_TREAT_BLANK_AS_UNDECLARED_WRITE_IN = false;
static final int SUGGESTED_CVR_FIRST_VOTE_COLUMN = 4;
static final int SUGGESTED_CVR_FIRST_VOTE_ROW = 2;
static final int SUGGESTED_CVR_ID_COLUMN = 1;
static final int SUGGESTED_CVR_PRECINCT_COLUMN = 2;
static final int SUGGESTED_NUMBER_OF_WINNERS = 1;
static final int SUGGESTED_DECIMAL_PLACES_FOR_VOTE_ARITHMETIC = 4;
static final int SUGGESTED_MAX_SKIPPED_RANKS_ALLOWED = 1;
static final String SUGGESTED_OVERVOTE_LABEL = "overvote";
static final String SUGGESTED_UNDERVOTE_LABEL = "undervote";
static final String UNDECLARED_WRITE_INS = "Undeclared Write-ins";
private static final int MIN_COLUMN_INDEX = 1;
private static final int MAX_COLUMN_INDEX = 1000;
Expand Down Expand Up @@ -306,7 +312,7 @@ private static boolean stringMatchesAnotherFieldValue(
}

private static void logErrorWithLocation(String message, String inputLocation) {
message += inputLocation == null ? "!" : ": " + inputLocation;
message += inputLocation == null ? "!" : "for file source: " + inputLocation;
Logger.log(Level.SEVERE, message);
}

Expand Down Expand Up @@ -548,10 +554,9 @@ private void validateCvrFileSources() {
}
} else if (getOvervoteRule() == OvervoteRule.EXHAUST_IF_MULTIPLE_CONTINUING) {
isValid = false;
Logger.log(
Level.SEVERE,
"overvoteDelimiter is required for an ES&S CVR source when overvoteRule is set "
+ "to exhaustIfMultipleContinuing.");
Logger.log(Level.SEVERE,
"overvoteDelimiter is required for an ES&S CVR source when overvoteRule is set to \"%s\".",
Tabulator.OVERVOTE_RULE_EXHAUST_IF_MULTIPLE_TEXT);
}
}
}
Expand Down Expand Up @@ -625,10 +630,10 @@ private void validateRules() {
&& getOvervoteRule() != Tabulator.OvervoteRule.EXHAUST_IMMEDIATELY
&& getOvervoteRule() != Tabulator.OvervoteRule.ALWAYS_SKIP_TO_NEXT_RANK) {
isValid = false;
Logger.log(
Level.SEVERE,
"When overvoteLabel is supplied, overvoteRule must be either exhaustImmediately "
+ "or alwaysSkipToNextRank!");
Logger.log(Level.SEVERE,
"When overvoteLabel is supplied, overvoteRule must be either \"%s\" or \"%s\"!",
Tabulator.OVERVOTE_RULE_ALWAYS_SKIP_TEXT,
Tabulator.OVERVOTE_RULE_EXHAUST_IF_MULTIPLE_TEXT);
}

if (getWinnerElectionMode() == WinnerElectionMode.MODE_UNKNOWN) {
Expand Down
86 changes: 71 additions & 15 deletions src/main/java/network/brightspots/rcv/GuiConfigController.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.MenuBar;
import javafx.scene.control.RadioButton;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableColumn;
Expand Down Expand Up @@ -162,7 +163,11 @@ public class GuiConfigController implements Initializable {
@FXML
private ChoiceBox<TieBreakMode> choiceTiebreakMode;
@FXML
private ChoiceBox<OvervoteRule> choiceOvervoteRule;
private RadioButton radioOvervoteAlwaysSkip;
@FXML
private RadioButton radioOvervoteExhaustImmediately;
@FXML
private RadioButton radioOvervoteExhaustIfMultiple;
@FXML
private ChoiceBox<WinnerElectionMode> choiceWinnerElectionMode;
@FXML
Expand Down Expand Up @@ -220,9 +225,16 @@ private static TieBreakMode getTiebreakModeChoice(ChoiceBox<TieBreakMode> choice
: TieBreakMode.MODE_UNKNOWN;
}

private static OvervoteRule getOvervoteRuleChoice(ChoiceBox<OvervoteRule> choiceBox) {
return choiceBox.getValue() != null ? OvervoteRule.getByLabel(choiceBox.getValue().toString())
: OvervoteRule.RULE_UNKNOWN;
private String getOvervoteRuleChoice() {
String overvoteRuleString = OvervoteRule.RULE_UNKNOWN.toString();
if (radioOvervoteAlwaysSkip.isSelected()) {
overvoteRuleString = OvervoteRule.ALWAYS_SKIP_TO_NEXT_RANK.toString();
} else if (radioOvervoteExhaustImmediately.isSelected()) {
overvoteRuleString = OvervoteRule.EXHAUST_IMMEDIATELY.toString();
} else if (radioOvervoteExhaustIfMultiple.isSelected()) {
overvoteRuleString = OvervoteRule.EXHAUST_IF_MULTIPLE_CONTINUING.toString();
}
return overvoteRuleString;
}

private static String getTextOrEmptyString(TextField textField) {
Expand Down Expand Up @@ -706,6 +718,8 @@ private void setDefaultValues() {
ContestConfig.SUGGESTED_TREAT_BLANK_AS_UNDECLARED_WRITE_IN);

setWinningRulesDefaultValues();
textFieldOvervoteLabel.setText(ContestConfig.SUGGESTED_OVERVOTE_LABEL);
textFieldUndervoteLabel.setText(ContestConfig.SUGGESTED_UNDERVOTE_LABEL);

textFieldMaxSkippedRanksAllowed.setText(
String.valueOf(ContestConfig.SUGGESTED_MAX_SKIPPED_RANKS_ALLOWED));
Expand Down Expand Up @@ -741,7 +755,9 @@ private void clearConfig() {
textFieldUndeclaredWriteInLabel.clear();
checkBoxTreatBlankAsUndeclaredWriteIn.setSelected(false);

choiceOvervoteRule.setValue(null);
radioOvervoteAlwaysSkip.setSelected(false);
radioOvervoteExhaustImmediately.setSelected(false);
radioOvervoteExhaustIfMultiple.setSelected(false);
textFieldMaxSkippedRanksAllowed.clear();
checkBoxExhaustOnDuplicateCandidate.setSelected(false);

Expand Down Expand Up @@ -897,9 +913,16 @@ public LocalDate fromString(String string) {
textFieldCvrFilePath.setDisable(false);
buttonCvrFilePath.setDisable(false);
textFieldCvrFirstVoteCol.setDisable(false);
textFieldCvrFirstVoteCol
.setText(String.valueOf(ContestConfig.SUGGESTED_CVR_FIRST_VOTE_COLUMN));
textFieldCvrFirstVoteRow.setDisable(false);
textFieldCvrFirstVoteRow
.setText(String.valueOf(ContestConfig.SUGGESTED_CVR_FIRST_VOTE_ROW));
textFieldCvrIdCol.setDisable(false);
textFieldCvrIdCol.setText(String.valueOf(ContestConfig.SUGGESTED_CVR_ID_COLUMN));
textFieldCvrPrecinctCol.setDisable(false);
textFieldCvrPrecinctCol
.setText(String.valueOf(ContestConfig.SUGGESTED_CVR_PRECINCT_COLUMN));
textFieldCvrOvervoteDelimiter.setDisable(false);
}
case CLEAR_BALLOT, DOMINION, HART, CDF -> {
Expand Down Expand Up @@ -960,8 +983,6 @@ public LocalDate fromString(String string) {
.setDisable(false);
}
});
choiceOvervoteRule.getItems().addAll(OvervoteRule.values());
choiceOvervoteRule.getItems().remove(OvervoteRule.RULE_UNKNOWN);
choiceWinnerElectionMode.getItems().addAll(WinnerElectionMode.values());
choiceWinnerElectionMode.getItems().remove(WinnerElectionMode.MODE_UNKNOWN);
choiceWinnerElectionMode.setOnAction(event -> {
Expand All @@ -972,13 +993,11 @@ public LocalDate fromString(String string) {
textFieldMaxRankingsAllowed.setDisable(false);
textFieldMinimumVoteThreshold.setDisable(false);
choiceTiebreakMode.setDisable(false);
checkBoxNonIntegerWinningThreshold.setDisable(false);
checkBoxHareQuota.setDisable(false);
textFieldDecimalPlacesForVoteArithmetic.setDisable(false);
checkBoxBatchElimination.setDisable(false);
checkBoxContinueUntilTwoCandidatesRemain.setDisable(false);
}
case MULTI_SEAT_ALLOW_ONLY_ONE_WINNER_PER_ROUND, MULTI_SEAT_ALLOW_MULTIPLE_WINNERS_PER_ROUND, MULTI_SEAT_BOTTOMS_UP_UNTIL_N_WINNERS, MULTI_SEAT_SEQUENTIAL_WINNER_TAKES_ALL -> {
case MULTI_SEAT_ALLOW_ONLY_ONE_WINNER_PER_ROUND, MULTI_SEAT_ALLOW_MULTIPLE_WINNERS_PER_ROUND -> {
textFieldMaxRankingsAllowed.setDisable(false);
textFieldMinimumVoteThreshold.setDisable(false);
choiceTiebreakMode.setDisable(false);
Expand All @@ -987,19 +1006,28 @@ public LocalDate fromString(String string) {
textFieldDecimalPlacesForVoteArithmetic.setDisable(false);
textFieldNumberOfWinners.setDisable(false);
}
case MULTI_SEAT_BOTTOMS_UP_UNTIL_N_WINNERS, MULTI_SEAT_SEQUENTIAL_WINNER_TAKES_ALL -> {
textFieldMaxRankingsAllowed.setDisable(false);
textFieldMinimumVoteThreshold.setDisable(false);
choiceTiebreakMode.setDisable(false);
checkBoxHareQuota.setDisable(false);
textFieldNumberOfWinners.setDisable(false);
}
case MULTI_SEAT_BOTTOMS_UP_USING_PERCENTAGE_THRESHOLD -> {
textFieldMaxRankingsAllowed.setDisable(false);
textFieldMinimumVoteThreshold.setDisable(false);
choiceTiebreakMode.setDisable(false);
checkBoxNonIntegerWinningThreshold.setDisable(false);
checkBoxHareQuota.setDisable(false);
textFieldDecimalPlacesForVoteArithmetic.setDisable(false);
textFieldNumberOfWinners.setDisable(false);
textFieldMultiSeatBottomsUpPercentageThreshold.setDisable(false);
}
}
});

radioOvervoteAlwaysSkip.setText(Tabulator.OVERVOTE_RULE_ALWAYS_SKIP_TEXT);
radioOvervoteExhaustImmediately.setText(Tabulator.OVERVOTE_RULE_EXHAUST_IMMEDIATELY_TEXT);
radioOvervoteExhaustIfMultiple.setText(Tabulator.OVERVOTE_RULE_EXHAUST_IF_MULTIPLE_TEXT);

setDefaultValues();

try {
Expand Down Expand Up @@ -1072,6 +1100,24 @@ private void migrateConfigVersion(ContestConfig config) {
}
}

if (config.getOvervoteRule() == OvervoteRule.RULE_UNKNOWN) {
String oldOvervoteRule = config.rawConfig.rules.overvoteRule;
switch (oldOvervoteRule) {
case "alwaysSkipToNextRank" -> config.rawConfig.rules.overvoteRule = OvervoteRule.ALWAYS_SKIP_TO_NEXT_RANK
.toString();
case "exhaustImmediately" -> config.rawConfig.rules.overvoteRule = OvervoteRule.EXHAUST_IMMEDIATELY
.toString();
case "exhaustIfMultipleContinuing" -> config.rawConfig.rules.overvoteRule = OvervoteRule.EXHAUST_IF_MULTIPLE_CONTINUING
.toString();
default -> {
Logger.log(Level.WARNING,
"overvoteRule \"%s\" is unrecognized! Please supply a valid overvoteRule.",
oldOvervoteRule);
config.rawConfig.rules.overvoteRule = null;
}
}
}

Logger.log(
Level.INFO,
"Migrated tabulator config version from %s to %s.",
Expand Down Expand Up @@ -1116,8 +1162,7 @@ private void loadConfig(ContestConfig config) {
: config.getWinnerElectionMode());
choiceTiebreakMode.setValue(
config.getTiebreakMode() == TieBreakMode.MODE_UNKNOWN ? null : config.getTiebreakMode());
choiceOvervoteRule.setValue(
config.getOvervoteRule() == OvervoteRule.RULE_UNKNOWN ? null : config.getOvervoteRule());
setOvervoteRuleRadioButton(config.getOvervoteRule());

ContestRules rules = rawConfig.rules;
textFieldRandomSeed.setText(rules.randomSeed);
Expand All @@ -1140,6 +1185,17 @@ private void loadConfig(ContestConfig config) {
checkBoxTreatBlankAsUndeclaredWriteIn.setSelected(rules.treatBlankAsUndeclaredWriteIn);
}

private void setOvervoteRuleRadioButton(OvervoteRule overvoteRule) {
switch (overvoteRule) {
case ALWAYS_SKIP_TO_NEXT_RANK -> radioOvervoteAlwaysSkip.setSelected(true);
case EXHAUST_IMMEDIATELY -> radioOvervoteExhaustImmediately.setSelected(true);
case EXHAUST_IF_MULTIPLE_CONTINUING -> radioOvervoteExhaustIfMultiple.setSelected(true);
case RULE_UNKNOWN -> {
// Do nothing for unknown overvote rules
}
}
}

private RawContestConfig createRawContestConfig() {
RawContestConfig config = new RawContestConfig();
config.tabulatorVersion = Main.APP_VERSION;
Expand Down Expand Up @@ -1177,7 +1233,7 @@ private RawContestConfig createRawContestConfig() {

ContestRules rules = new ContestRules();
rules.tiebreakMode = getTiebreakModeChoice(choiceTiebreakMode).toString();
rules.overvoteRule = getOvervoteRuleChoice(choiceOvervoteRule).toString();
rules.overvoteRule = getOvervoteRuleChoice();
rules.winnerElectionMode = getWinnerElectionModeChoice(choiceWinnerElectionMode).toString();
rules.randomSeed = getTextOrEmptyString(textFieldRandomSeed);
rules.numberOfWinners = getTextOrEmptyString(textFieldNumberOfWinners);
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/network/brightspots/rcv/Tabulator.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@

class Tabulator {

static final String OVERVOTE_RULE_ALWAYS_SKIP_TEXT = "Always skip to next rank";
static final String OVERVOTE_RULE_EXHAUST_IMMEDIATELY_TEXT = "Exhaust immediately";
static final String OVERVOTE_RULE_EXHAUST_IF_MULTIPLE_TEXT = "Exhaust if multiple continuing";
// When the CVR contains an overvote we "normalize" it to use this string
static final String EXPLICIT_OVERVOTE_LABEL = "overvote";
// cast vote records parsed from CVR input files
Expand Down Expand Up @@ -1065,10 +1068,10 @@ private void initPrecinctRoundTallies() {

// OvervoteRule determines how overvotes are handled
enum OvervoteRule {
EXHAUST_IMMEDIATELY("exhaustImmediately"),
ALWAYS_SKIP_TO_NEXT_RANK("alwaysSkipToNextRank"),
EXHAUST_IF_MULTIPLE_CONTINUING("exhaustIfMultipleContinuing"),
RULE_UNKNOWN("ruleUnknown");
ALWAYS_SKIP_TO_NEXT_RANK(OVERVOTE_RULE_ALWAYS_SKIP_TEXT),
EXHAUST_IMMEDIATELY(OVERVOTE_RULE_EXHAUST_IMMEDIATELY_TEXT),
EXHAUST_IF_MULTIPLE_CONTINUING(OVERVOTE_RULE_EXHAUST_IF_MULTIPLE_TEXT),
RULE_UNKNOWN("Unknown rule");

private final String label;

Expand Down
19 changes: 16 additions & 3 deletions src/main/resources/network/brightspots/rcv/GuiConfigLayout.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SeparatorMenuItem?>
Expand All @@ -34,6 +35,7 @@
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
Expand Down Expand Up @@ -467,7 +469,8 @@
</padding>
</HBox>
<HBox alignment="CENTER_LEFT" spacing="4.0">
<Label text="Decimal Places for Vote Arithmetic *" prefWidth="196.0"/>
<Label prefWidth="196.0"
text="Decimal Places for Vote Arithmetic&#xD; (Multi-Winner Only) *"/>
<TextField fx:id="textFieldDecimalPlacesForVoteArithmetic" maxWidth="220.0"/>
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
Expand Down Expand Up @@ -530,9 +533,19 @@
<Tab text="Voter Error Rules">
<VBox>
<VBox styleClass="bordered-box" maxWidth="418.0">
<HBox alignment="CENTER_LEFT" spacing="4.0">
<HBox spacing="4.0">
<Label text="Overvote Rule *" prefWidth="160.0"/>
<ChoiceBox fx:id="choiceOvervoteRule" prefWidth="220.0"/>
<VBox spacing="4.0">
<fx:define>
<ToggleGroup fx:id="overvoteRuleToggleGroup"/>
</fx:define>
<RadioButton mnemonicParsing="false" toggleGroup="$overvoteRuleToggleGroup"
fx:id="radioOvervoteAlwaysSkip"/>
<RadioButton mnemonicParsing="false" toggleGroup="$overvoteRuleToggleGroup"
fx:id="radioOvervoteExhaustImmediately"/>
<RadioButton mnemonicParsing="false" toggleGroup="$overvoteRuleToggleGroup"
fx:id="radioOvervoteExhaustIfMultiple"/>
</VBox>
<padding>
<Insets bottom="4.0" left="4.0" right="4.0" top="4.0"/>
</padding>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,10 @@ Config file must be valid JSON format. Examples can be found in the test_data fo

"overvoteRule" required
how the program should handle an overvote when it encounters one
value: "alwaysSkipToNextRank" | "exhaustImmediately" | "exhaustIfMultipleContinuing"
"alwaysSkipToNextRank": when we encounter an overvote, ignore this rank and look at the next rank in the cast vote record
"exhaustImmediately": exhaust a ballot as soon as we encounter an overvote
"exhaustIfMultipleContinuing": if more than one candidate in an overvote are continuing, exhaust the ballot; if only one, assign the vote to them; if none, continue to the next rank (not valid with an ES&S source unless overvoteDelimiter is supplied)
value: "Always skip to next rank" | "Exhaust immediately" | "Exhaust if multiple continuing"
"Always skip to next rank": when we encounter an overvote, ignore this rank and look at the next rank in the cast vote record
"Exhaust immediately": exhaust a ballot as soon as we encounter an overvote
"Exhaust if multiple continuing": if more than one candidate in an overvote are continuing, exhaust the ballot; if only one, assign the vote to them; if none, continue to the next rank (not valid with an ES&S source unless overvoteDelimiter is supplied)

"winnerElectionMode" required
whether the program should apply a special process for selecting the winner(s)
Expand All @@ -189,6 +189,7 @@ Config file must be valid JSON format. Examples can be found in the test_data fo

"decimalPlacesForVoteArithmetic" required
number of rounding decimal places when computing winning thresholds and fractional vote transfers
note: only matters when winnerElectionMode is "Multi-winner allow only one winner per round" or "Multi-winner allow multiple winners per round"
value: [1..20]

"minimumVoteThreshold" optional
Expand All @@ -212,7 +213,7 @@ Config file must be valid JSON format. Examples can be found in the test_data fo
value: [-140737488355328..140737488355327]

"overvoteLabel" optional
label used in the CVR to denote an overvote; if this parameter is present overvoteRule must be either "alwaysSkipToNextRank" or "exhaustImmediately" (because the other option, exhaustIfMultipleContinuing, relies on knowing which specific candidates were involved in each overvote)
label used in the CVR to denote an overvote; if this parameter is present overvoteRule must be either "Always skip to next rank" or "Exhaust immediately" (because the other option, "Exhaust if multiple continuing", relies on knowing which specific candidates were involved in each overvote)
example: "OVERVOTE"
value: string of length [1..1000]

Expand Down Expand Up @@ -243,17 +244,19 @@ Config file must be valid JSON format. Examples can be found in the test_data fo
if false, threshold = floor(V/(S+1)) + 1
where V = total number of votes (in the current round for single-seat or in the first round for multi-seat); S = numberOfWinners; and d = decimalPlacesForVoteArithmetic
(note that S+1 in the formulas above becomes just S if hareQuota is set to true)
note: only valid for multi-seat contests
value: true | false
if not supplied: false

"hareQuota" optional
the winning threshold should be computed using the Hare quota (votes divided by seats) instead of the preferred Droop quota (votes divided by (seats+1))
only valid for multi-seat contests
note: only valid for multi-seat contests
value: true | false
if not supplied: false

"batchElimination" optional
tabulator will use batch elimination (only valid for single-winner contests)
tabulator will use batch elimination
note: only valid for single-winner contests
value: true | false
if not supplied: false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
} ],
"rules" : {
"tiebreakMode": "Random",
"overvoteRule": "exhaustImmediately",
"overvoteRule": "Exhaust immediately",
"winnerElectionMode": "Single-winner majority determines winner",
"randomSeed": "0",
"numberOfWinners": "1",
Expand Down
Loading