diff --git a/.idea/misc.xml b/.idea/misc.xml index f16dea7..ef46287 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/src/main/java/org/awdevelopment/smithlab/config/AbstractConfig.java b/src/main/java/org/awdevelopment/smithlab/config/AbstractConfig.java index 89ae578..9116180 100644 --- a/src/main/java/org/awdevelopment/smithlab/config/AbstractConfig.java +++ b/src/main/java/org/awdevelopment/smithlab/config/AbstractConfig.java @@ -79,6 +79,14 @@ private void setConfigEntry(ConfigOption option, T value) throws ConfigExcep @Override public abstract String toString(); + public String readableConfig() { + StringBuilder sb = new StringBuilder(); + for (ConfigEntry entry : config) { + sb.append("Option: ").append(entry.option()).append("\tValue: ").append(entry.value()).append("\n"); + } + return sb.toString(); + } + public abstract Mode mode(); public LoggerHelper LOGGER() { return LOGGER; } diff --git a/src/main/java/org/awdevelopment/smithlab/config/EmptyInputSheetConfig.java b/src/main/java/org/awdevelopment/smithlab/config/EmptyInputSheetConfig.java index 05e98ea..e84ce2d 100644 --- a/src/main/java/org/awdevelopment/smithlab/config/EmptyInputSheetConfig.java +++ b/src/main/java/org/awdevelopment/smithlab/config/EmptyInputSheetConfig.java @@ -78,6 +78,7 @@ public EmptyInputSheetConfig(Arguments args, LoggerHelper logger) { @Override public String toString() { return "Empty Input Sheet Configuration"; } + @Override public Mode mode() { return MODE; }; } diff --git a/src/main/java/org/awdevelopment/smithlab/data/Condition.java b/src/main/java/org/awdevelopment/smithlab/data/Condition.java index a8dd0fb..61f70e7 100644 --- a/src/main/java/org/awdevelopment/smithlab/data/Condition.java +++ b/src/main/java/org/awdevelopment/smithlab/data/Condition.java @@ -1,18 +1,22 @@ package org.awdevelopment.smithlab.data; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; public class Condition { - private final String name; + private String name; private final Set strains; private final Set samples; public Condition(String name) { this.name = name; - this.strains = new HashSet<>(); - this.samples = new HashSet<>(); + this.strains = new LinkedHashSet<>(); + this.samples = new LinkedHashSet<>(); + } + + public void setName(String name) { + this.name = name; } public void addStrain(Strain strain) { @@ -37,4 +41,6 @@ public Set getSamples() { public String toString() { return name; } + + } diff --git a/src/main/java/org/awdevelopment/smithlab/data/Strain.java b/src/main/java/org/awdevelopment/smithlab/data/Strain.java index 02c3f69..ba1ca50 100644 --- a/src/main/java/org/awdevelopment/smithlab/data/Strain.java +++ b/src/main/java/org/awdevelopment/smithlab/data/Strain.java @@ -7,7 +7,7 @@ public class Strain { private Condition condition; - private final String name; + private String name; private final Set samples; public Strain(String name) { @@ -15,6 +15,7 @@ public Strain(String name) { samples = new HashSet<>(); } + public void setName(String name) { this.name = name; } public String getName() { return name; } @@ -40,11 +41,18 @@ public boolean equals(Object obj) { return false; } Strain strain = (Strain) obj; - return name.equals(strain.name) && condition.equals(strain.condition); + if (condition == null) { + if (strain.condition != null) { + return false; + } + } else if (!condition.equals(strain.condition)) { + return false; + } + return name.equals(strain.name); } @Override public int hashCode() { - return name.hashCode() + condition.hashCode(); + return name.hashCode() + (condition != null ? condition.hashCode() : 0); } } diff --git a/src/main/java/org/awdevelopment/smithlab/data/experiment/EmptyExperiment.java b/src/main/java/org/awdevelopment/smithlab/data/experiment/EmptyExperiment.java index fa8dc79..1270e34 100644 --- a/src/main/java/org/awdevelopment/smithlab/data/experiment/EmptyExperiment.java +++ b/src/main/java/org/awdevelopment/smithlab/data/experiment/EmptyExperiment.java @@ -2,7 +2,6 @@ import org.awdevelopment.smithlab.data.Condition; import org.awdevelopment.smithlab.data.Strain; -import org.awdevelopment.smithlab.io.exceptions.NoDaysException; import java.util.Set; @@ -17,85 +16,20 @@ public class EmptyExperiment extends AbstractExperiment { private final byte numDays; private final boolean usingNumDays; - public EmptyExperiment(Set strains, Set conditions, byte numReplicates, byte[] days, byte numDays) throws NoDaysException { + public EmptyExperiment(Set conditions, Set strains, byte numReplicates, byte numConditions, + byte numStrains, boolean usingNumConditions, boolean usingNumStrains, byte[] days, + byte numDays, boolean usingNumDays) { super(conditions, strains); - this.usingNumConditions = false; - this.usingNumStrains = false; - this.numConditions = (byte) conditions.size(); - this.numStrains = (byte) strains.size(); this.numReplicates = numReplicates; - if (numDays <= 0 && days.length == 0) throw new NoDaysException(); - else { - usingNumDays = numDays > 0; - if (usingNumDays) { - this.days = new byte[numDays]; - this.numDays = numDays; - } else { - this.days = days; - this.numDays = (byte) days.length; - } - } - } - - public EmptyExperiment(byte numStrains, byte numConditions, byte numReplicates, byte[] days, byte numDays) throws NoDaysException { - super(null, null); - this.usingNumConditions = true; - this.usingNumStrains = true; this.numConditions = numConditions; this.numStrains = numStrains; - this.numReplicates = numReplicates; - if (numDays <= 0 && days.length == 0) throw new NoDaysException(); - else { - usingNumDays = numDays > 0; - if (usingNumDays) { - this.days = new byte[numDays]; - this.numDays = numDays; - } else { - this.days = days; - this.numDays = (byte) days.length; - } - } - } - - public EmptyExperiment(Set strains, byte numConditions, byte numReplicates, byte[] days, byte numDays) throws NoDaysException { - super(null, strains); - this.usingNumConditions = false; - this.usingNumStrains = true; - this.numConditions = numConditions; - this.numStrains = (byte) strains.size(); - this.numReplicates = numReplicates; - if (numDays <= 0 && days.length == 0) throw new NoDaysException(); - else { - usingNumDays = numDays > 0; - if (usingNumDays) { - this.days = new byte[numDays]; - this.numDays = numDays; - } else { - this.days = days; - this.numDays = (byte) days.length; - } - } + this.usingNumConditions = usingNumConditions; + this.usingNumStrains = usingNumStrains; + this.days = days; + this.numDays = numDays; + this.usingNumDays = usingNumDays; } - public EmptyExperiment(byte numStrains, Set conditions, byte numReplicates, byte[] days, byte numDays) throws NoDaysException { - super(conditions, null); - this.usingNumConditions = true; - this.usingNumStrains = false; - this.numConditions = (byte) conditions.size(); - this.numStrains = numStrains; - this.numReplicates = numReplicates; - if (numDays <= 0 && days.length == 0) throw new NoDaysException(); - else { - usingNumDays = numDays > 0; - if (usingNumDays) { - this.days = new byte[numDays]; - this.numDays = numDays; - } else { - this.days = days; - this.numDays = (byte) days.length; - } - } - } public byte getNumReplicates() { return numReplicates; } public boolean hasNoDays() { return days.length == 0 && !usingNumDays; } @@ -106,4 +40,37 @@ public EmptyExperiment(byte numStrains, Set conditions, byte numRepli public boolean usingNumStrains() { return usingNumStrains; } public byte getNumConditions() { return numConditions; } public byte getNumStrains() { return numStrains; } + + public String readableExperiment() { + StringBuilder sb = new StringBuilder(); + sb.append("Experiment with "); + if (usingNumStrains) { + sb.append(numStrains).append(" strains, "); + } else { + StringBuilder strainsList = new StringBuilder(); + for (Strain strain : strains()) { strainsList.append(strain.getName()).append(", "); } + strainsList.setLength(Math.max(strainsList.length() - 2, 0)); // Remove trailing comma and space, if any + sb.append(strains().size()).append(" strains (").append(strainsList).append("), "); + } + if (usingNumConditions) { + sb.append(numConditions).append(" conditions, "); + } else { + StringBuilder conditionsList = new StringBuilder(); + for (Condition condition : conditions()) { conditionsList.append(condition.getName()).append(", "); } + conditionsList.setLength(Math.max(conditionsList.length() - 2, 0)); // Remove trailing comma and space, if any + sb.append(conditions().size()).append(" conditions (").append(conditionsList).append("), "); + } + sb.append(numReplicates).append(" replicates"); + if (usingNumDays) { + sb.append(", ").append(numDays).append(" days."); + } else if (days.length > 0) { + sb.append(", days: "); + for (byte day : days) { + sb.append(day).append(" "); + } + } else { + sb.append("."); + } + return sb.toString(); + } } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/SceneLoader.java b/src/main/java/org/awdevelopment/smithlab/gui/SceneLoader.java index 12ed49d..3914cfe 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/SceneLoader.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/SceneLoader.java @@ -3,6 +3,7 @@ import javafx.stage.Stage; import org.awdevelopment.smithlab.config.EmptyInputSheetConfig; import org.awdevelopment.smithlab.gui.controllers.AbstractLabelController; +import org.awdevelopment.smithlab.gui.controllers.main.MainApplicationController; import org.awdevelopment.smithlab.logging.LoggerHelper; public class SceneLoader { @@ -16,13 +17,17 @@ public static void loadScene(Stage stage, FXMLResourceType fxmlResourceType, Log stage.show(); } - public static AbstractLabelController loadScene(Stage stage, FXMLResourceType fxmlResourceType, LoggerHelper logger, EmptyInputSheetConfig config) { + public static AbstractLabelController loadScene(Stage stage, FXMLResourceType fxmlResourceType, LoggerHelper logger, + EmptyInputSheetConfig config, + MainApplicationController mainController) { FXMLResource fxmlResource = FXMLResourceLoader.load(fxmlResourceType, logger); fxmlResource.controller().setLogger(logger); ((AbstractLabelController) fxmlResource.controller()).setConfig(config); fxmlResource.controller().setup(); stage.setTitle(fxmlResourceType.getWindowTitle()); stage.setScene(fxmlResource.scene()); + AbstractLabelController controller = (AbstractLabelController) fxmlResource.controller(); + controller.setMainController(mainController); stage.show(); return (AbstractLabelController) fxmlResource.controller(); } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/AbstractLabelController.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/AbstractLabelController.java index f627bae..71d5e9b 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/AbstractLabelController.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/AbstractLabelController.java @@ -1,10 +1,12 @@ package org.awdevelopment.smithlab.gui.controllers; import org.awdevelopment.smithlab.config.EmptyInputSheetConfig; +import org.awdevelopment.smithlab.gui.controllers.main.MainApplicationController; public abstract class AbstractLabelController extends AbstractController { private EmptyInputSheetConfig config; + private MainApplicationController mainController; public AbstractLabelController() { super(); } @@ -12,7 +14,12 @@ public void setConfig(EmptyInputSheetConfig config) { this.config = config; } + public void setMainController(MainApplicationController mainController) { this.mainController = mainController; } + public EmptyInputSheetConfig config() { return config; } + + public MainApplicationController getMainController() { return mainController; } + } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/ConditionsController.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/ConditionsController.java index 2b0a810..764d71c 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/ConditionsController.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/ConditionsController.java @@ -1,28 +1,199 @@ package org.awdevelopment.smithlab.gui.controllers; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; import org.awdevelopment.smithlab.data.Condition; -import java.util.HashSet; -import java.util.Set; +import java.util.*; +/** + * Controller that maintains a dynamic list of TextFields so that: + * - There is always exactly one empty field at the bottom. + * - When the bottom field becomes non-empty, a new empty field is appended. + * - When a user clears a field and leaves it, it disappears if another empty bottom field exists. + */ public class ConditionsController extends AbstractLabelController { + @FXML private VBox conditionsBox; + @FXML private Button saveCloseButton; + private boolean usingNumConditions; - private final Set conditions; - public ConditionsController() { - super(); - conditions = new HashSet<>(); + private final Set conditions = new LinkedHashSet<>(); + private final Map textFieldToCondition = new HashMap<>(); + + // ----------------- lifecycle ----------------- + + @FXML + public void initialize() { + // Make sure we start with one empty field at the bottom + ensureOneBlankAtBottom(); + this.usingNumConditions = true; } @Override - public void setup() {} + public void setup() { } - public boolean usingNumConditions() { return usingNumConditions; } + // ----------------- public API ----------------- + + public boolean usingNumConditions() { + return usingNumConditions; + } public void setUsingNumConditions(boolean usingNumConditions) { this.usingNumConditions = usingNumConditions; } - public Set getConditions() { return conditions; } + public Set getConditions() { + return Collections.unmodifiableSet(conditions); + } + + /** + * Pre-populate the UI with known conditions. You may call this before the view is shown. + */ + public void loadConditions(Collection initialConditions) { + conditions.clear(); + conditions.addAll(initialConditions); + + conditionsBox.getChildren().clear(); + textFieldToCondition.clear(); + + for (Condition c : initialConditions) { + TextField tf = createField(c.getName()); + conditionsBox.getChildren().add(tf); + textFieldToCondition.put(tf, c); + } + ensureOneBlankAtBottom(); + } + + // ----------------- UI callbacks ----------------- + + @FXML + private void handleSaveAndClose(ActionEvent ignoredEvent) { + // Optionally, clean trailing empties (leaving exactly one at bottom): + pruneExtraEmptiesKeepOne(); + Stage stage = (Stage) saveCloseButton.getScene().getWindow(); + stage.close(); + } + + // ----------------- core logic ----------------- + + private TextField createField(String text) { + + TextField tf = new TextField(text); + + // Show placeholder only if the field is empty + if (text == null || text.isEmpty()) { + tf.setPromptText("Enter condition name..."); + } + + // React live to changes + tf.textProperty().addListener((obs, oldValue, newValue) -> onTextChanged(tf, newValue)); + + // On focus loss, maybe remove if empty + tf.focusedProperty().addListener((obs, wasFocused, isFocused) -> { + if (!isFocused) { + onFocusLost(tf); + } + }); + + return tf; + } + + private void onTextChanged(TextField tf, String newValue) { + String trimmed = newValue.trim(); + + if (trimmed.isEmpty()) { + // Remove mapping and condition if it existed + Condition cond = textFieldToCondition.remove(tf); + if (cond != null) { + conditions.remove(cond); + super.config().setConditions(conditions); + if (conditions.isEmpty()) { + this.setUsingNumConditions(true); + this.getMainController().enableNumConditions(); + } + } + } else { + // Non-empty: make sure a Condition exists and stays updated + Condition cond = textFieldToCondition.get(tf); + if (cond == null) { + cond = new Condition(trimmed); + textFieldToCondition.put(tf, cond); + conditions.add(cond); + super.config().setConditions(conditions); + super.getMainController().disableNumConditions(); + this.setUsingNumConditions(false); + } else if (!trimmed.equals(cond.getName())) { + cond.setName(trimmed); + } + + // If user typed into the last (blank) field, append a new blank + if (isLast(tf)) { + conditionsBox.getChildren().add(createField("")); + } + } + } + + private void onFocusLost(TextField tf) { + if (!tf.getText().trim().isEmpty()) { + return; // only care about empty fields + } + + // If it's not the only field AND there's another empty field at the bottom, remove it + if (conditionsBox.getChildren().size() > 1) { + TextField last = lastField(); + if (last != tf && last.getText().trim().isEmpty()) { + conditionsBox.getChildren().remove(tf); + } + } + + // Guarantee there is one (and exactly one) empty field at the bottom + ensureOneBlankAtBottom(); + } + + private void ensureOneBlankAtBottom() { + if (conditionsBox.getChildren().isEmpty()) { + conditionsBox.getChildren().add(createField("")); + return; + } + + TextField last = lastField(); + if (!last.getText().trim().isEmpty()) { + conditionsBox.getChildren().add(createField("")); + } else { + // Also ensure there aren't multiple blank fields at bottom accidentally + pruneExtraEmptiesKeepOne(); + } + } + + private void pruneExtraEmptiesKeepOne() { + List empties = new ArrayList<>(); + for (Node n : conditionsBox.getChildren()) { + if (n instanceof TextField tf && tf.getText().trim().isEmpty()) { + empties.add(tf); + } + } + // keep the *last* one, remove all previous empties + for (int i = 0; i < empties.size() - 1; i++) { + conditionsBox.getChildren().remove(empties.get(i)); + } + } + + private boolean isLast(TextField tf) { + ObservableList kids = conditionsBox.getChildren(); + return !kids.isEmpty() && kids.getLast() == tf; + } + + private TextField lastField() { + ObservableList kids = conditionsBox.getChildren(); + return (TextField) kids.getLast(); + } + } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/StrainsController.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/StrainsController.java index 8445f7b..34a883a 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/StrainsController.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/StrainsController.java @@ -1,25 +1,183 @@ package org.awdevelopment.smithlab.gui.controllers; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; import org.awdevelopment.smithlab.data.Strain; -import java.util.HashSet; -import java.util.Set; +import java.util.*; +/** + * Controller that maintains a dynamic list of TextFields so that: + * - There is always exactly one empty field at the bottom. + * - When the bottom field becomes non-empty, a new empty field is appended. + * - When a user clears a field and leaves it, it disappears if another empty bottom field exists. + */ public class StrainsController extends AbstractLabelController { + @FXML private VBox strainsBox; + @FXML private Button saveCloseButton; + private boolean usingNumStrains; - private final Set strains; - public StrainsController() { - super(); - strains = new HashSet<>(); + private final Set strains = new LinkedHashSet<>(); + private final Map textFieldToStrain = new HashMap<>(); + + // ----------------- lifecycle ----------------- + + @FXML + public void initialize() { + // Make sure we start with one empty field at the bottom + ensureOneBlankAtBottom(); + this.usingNumStrains = true; } @Override - public void setup() {} + public void setup() { } + + // ----------------- public API ----------------- + + public boolean usingNumStrains() { + return usingNumStrains; + } + + public void setUsingNumStrains(boolean usingNumStrains) { + this.usingNumStrains = usingNumStrains; + } + + public Set getStrains() { + return Collections.unmodifiableSet(strains); + } + + /** + * Pre-populate the UI with known strains. You may call this before the view is shown. + */ + public void loadStrains(Collection initialStrains) { + strains.clear(); + strains.addAll(initialStrains); + + strainsBox.getChildren().clear(); + textFieldToStrain.clear(); + + for (Strain s : initialStrains) { + TextField tf = createField(s.getName()); + strainsBox.getChildren().add(tf); + textFieldToStrain.put(tf, s); + } + ensureOneBlankAtBottom(); + } + + // ----------------- UI callbacks ----------------- + + @FXML + private void handleSaveAndClose(ActionEvent ignoredEvent) { + pruneExtraEmptiesKeepOne(); + Stage stage = (Stage) saveCloseButton.getScene().getWindow(); + stage.close(); + } + + // ----------------- core logic ----------------- + + private TextField createField(String text) { + TextField tf = new TextField(text); - public boolean usingNumStrains() { return usingNumStrains; } + if (text == null || text.isEmpty()) { + tf.setPromptText("Enter strain name..."); + } - public void setUsingNumStrains(boolean usingNumStrains) { this.usingNumStrains = usingNumStrains; } - public Set getStrains() { return strains; } + tf.textProperty().addListener((obs, oldValue, newValue) -> onTextChanged(tf, newValue)); + tf.focusedProperty().addListener((obs, wasFocused, isFocused) -> { + if (!isFocused) { + onFocusLost(tf); + } + }); + + return tf; + } + + private void onTextChanged(TextField tf, String newValue) { + String trimmed = newValue.trim(); + + if (trimmed.isEmpty()) { + Strain strain = textFieldToStrain.remove(tf); + if (strain != null) { + strains.remove(strain); + super.config().setStrains(strains); + if (strains.isEmpty()) { + this.setUsingNumStrains(true); + this.getMainController().enableNumStrains(); + } + } + } else { + Strain strain = textFieldToStrain.get(tf); + if (strain == null) { + strain = new Strain(trimmed); + textFieldToStrain.put(tf, strain); + strains.add(strain); + super.config().setStrains(strains); + super.getMainController().disableNumStrains(); + this.setUsingNumStrains(false); + } else if (!trimmed.equals(strain.getName())) { + strain.setName(trimmed); + } + + if (isLast(tf)) { + strainsBox.getChildren().add(createField("")); + } + } + } + + private void onFocusLost(TextField tf) { + if (!tf.getText().trim().isEmpty()) return; + + if (strainsBox.getChildren().size() > 1) { + TextField last = lastField(); + if (last != tf && last.getText().trim().isEmpty()) { + strainsBox.getChildren().remove(tf); + } + } + + ensureOneBlankAtBottom(); + } + + private void ensureOneBlankAtBottom() { + if (strainsBox.getChildren().isEmpty()) { + strainsBox.getChildren().add(createField("")); + return; + } + + TextField last = lastField(); + if (!last.getText().trim().isEmpty()) { + strainsBox.getChildren().add(createField("")); + } else { + pruneExtraEmptiesKeepOne(); + } + } + + private void pruneExtraEmptiesKeepOne() { + List empties = new ArrayList<>(); + for (Node n : strainsBox.getChildren()) { + if (n instanceof TextField tf && tf.getText().trim().isEmpty()) { + empties.add(tf); + } + } + for (int i = 0; i < empties.size() - 1; i++) { + strainsBox.getChildren().remove(empties.get(i)); + } + } + + private boolean isLast(TextField tf) { + ObservableList kids = strainsBox.getChildren(); + return !kids.isEmpty() && kids.getLast() == tf; + } + + private TextField lastField() { + ObservableList kids = strainsBox.getChildren(); + return (TextField) kids.getLast(); + } } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/TimepointsController.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/TimepointsController.java index 3860aa1..e378f15 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/TimepointsController.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/TimepointsController.java @@ -5,7 +5,7 @@ public class TimepointsController extends AbstractLabelController { - private boolean usingNumDays = false; + private boolean usingNumDays = true; // Default to using num days private final Set days; public TimepointsController() { diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractConfigUpdater.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractConfigUpdater.java index 7d86867..fe2f744 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractConfigUpdater.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractConfigUpdater.java @@ -113,18 +113,47 @@ else if (keyEvent != null && (!keyEvent.getText().isEmpty() || keyEvent.getCode( } } + boolean updateControllerConnectedField(ValidatableField field, ConfigOption option, KeyEvent keyEvent) { + LOGGER.atDebug("Updating controller connected field with id: \"" + field.getControlID() + "\"..."); + updateTextField(field, option, keyEvent); + // If the field is not unused and has been validated, return true - this indicates that we are using the number of conditions/strains/days + if (field.status() != FieldStatus.UNUSED) { + return true; + } + // If the field is unused, return false - this indicates that we are not using the number of conditions/strains/days + LOGGER.atDebug("Field with id: \"" + field.getControlID() + "\" is unused, returning false."); + return false; + } + void updateIfValidated(ValidatableField field, ConfigOption option) { LOGGER.atDebug("Checking if field was validated successfully..."); if (field.status() == FieldStatus.READY) { LOGGER.atDebug("Field was validated successfully, updating stored value..."); switch (field.getFieldType()) { - case BYTE -> config.set(option, Byte.parseByte(getTextField(field).getText())); + case BYTE -> { + config.set(option, Byte.parseByte(getTextField(field).getText())); + if (config.get(option) == ConfigOption.NUM_DAYS) { + config.set(ConfigOption.USING_NUM_DAYS, true); + + } else if (config.get(option) == ConfigOption.NUM_CONDITIONS) { + config.set(ConfigOption.USING_NUM_CONDITIONS, true); + } else if (config.get(option) == ConfigOption.NUM_STRAINS) { + config.set(ConfigOption.USING_NUM_STRAINS, true); + } + } case FILENAME, STRING -> config.set(option, getTextField(field).getText()); case EXISTING_FILE -> config.set(option, new File(getTextField(field).getText())); default -> throw new IllegalStateException("Unexpected value: " + field.getFieldType()); } LOGGER.atDebug("Stored value updated successfully."); - } else { + } else if (field.status() == FieldStatus.UNUSED) { + if (option == ConfigOption.NUM_DAYS) { + config.set(ConfigOption.USING_NUM_DAYS, false); + } else if (option == ConfigOption.NUM_CONDITIONS) { + config.set(ConfigOption.USING_NUM_CONDITIONS, false); + } else if (option == ConfigOption.NUM_STRAINS) { + config.set(ConfigOption.USING_NUM_STRAINS, false); + } LOGGER.atDebug("Field was not validated successfully, not updating stored value."); } } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractValidator.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractValidator.java index 7419f48..df3fccb 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractValidator.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/AbstractValidator.java @@ -44,6 +44,10 @@ public boolean fieldsValid() { validateTextFieldByID(field.getControlID()); } else if (field.status() == FieldStatus.EDITED_NOT_VALIDATED) { validateTextFieldByID(field.getControlID()); + } else if (field.status() == FieldStatus.UNUSED) { + LOGGER.atDebug("Field with id: \"" + field.getControlID() + "\" is unused."); + LOGGER.atDebug("Field status: \"" + field.status()); + LOGGER.atDebug("Continuing field validation..."); } if (field.status() == FieldStatus.INVALID || field.status() == FieldStatus.EMPTY) { LOGGER.atDebug("Field with id: \"" + field.getControlID() + "\" is invalid or empty."); @@ -62,11 +66,12 @@ void validateTextFieldNotEmpty(ValidatableField field) { TextField textField = getTextField(field); if (textField.getText().isEmpty()) { field.setStatus(FieldStatus.INVALID); - guiLogger.errorOccurred(field.getErrorLabel(), "Error: Please enter a value"); LOGGER.atDebug("Field with id: \"" + field.getControlID() + "\" is empty."); + LOGGER.atDebug("Error Label: Label object with ID \"" + field.getErrorLabel().getAccessibleHelp() + "\""); + guiLogger.errorOccurred(field.getErrorLabel(), "Error: Please enter a value"); } else { - guiLogger.clearError(field.getErrorLabel()); LOGGER.atDebug("Field with id: \"" + field.getControlID() + "\" is not empty."); + guiLogger.clearError(field.getErrorLabel()); field.setStatus(FieldStatus.READY); } } @@ -104,7 +109,6 @@ void validateTextFieldFilename(ValidatableField field) { } void validateTextFieldByte(ValidatableField field) { - if (field.status() == FieldStatus.UNTOUCHED) return; validateTextFieldNotEmpty(field); TextField textField = getTextField(field); try { diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetConfigUpdater.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetConfigUpdater.java index 43c5455..505031d 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetConfigUpdater.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetConfigUpdater.java @@ -32,7 +32,7 @@ public void updateFields() { } public void updateOutputFilename(KeyEvent keyEvent) { - updateTextField(fields.getOutputFilenameTextField(), ConfigOption.OUTPUT_FILE, keyEvent); + updateTextField(fields.getOutputFilenameTextField(), ConfigOption.EMPTY_INPUT_SHEET_NAME, keyEvent); } public void updateNumReplicates(KeyEvent keyEvent) { @@ -40,15 +40,15 @@ public void updateNumReplicates(KeyEvent keyEvent) { } public void updateNumTimepoints(KeyEvent keyEvent) { - updateTextField(fields.getNumTimepointsTextField(), ConfigOption.NUM_DAYS, keyEvent); + config.setUsingNumDays(updateControllerConnectedField(fields.getNumTimepointsTextField(), ConfigOption.NUM_DAYS, keyEvent)); } public void updateNumConditions(KeyEvent keyEvent) { - updateTextField(fields.getNumConditionsTextField(), ConfigOption.NUM_CONDITIONS, keyEvent); + config.setUsingNumConditions(updateControllerConnectedField(fields.getNumConditionsTextField(), ConfigOption.NUM_CONDITIONS, keyEvent)); } public void updateNumStrains(KeyEvent keyEvent) { - updateTextField(fields.getNumStrainsTextField(), ConfigOption.NUM_STRAINS, keyEvent); + config.setUsingNumStrains(updateControllerConnectedField(fields.getNumStrainsTextField(), ConfigOption.NUM_STRAINS, keyEvent)); } public void updateSampleLabelingRadioButtons(ActionEvent actionEvent) { diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetFields.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetFields.java index 33dd560..c1956f5 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetFields.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetFields.java @@ -21,7 +21,7 @@ public class EmptyInputSheetFields extends AbstractFields { private final ValidatableField sampleSortingMethodChoiceBox; EmptyInputSheetFields(MainApplicationController controller) { - super(controller, new ValidatableField[] { new ValidatableField(controller.numTimepointsTextField, controller.timepointsAddedLabel, FieldType.BYTE), + super(controller, new ValidatableField[] { new ValidatableField(controller.numTimepointsTextField, controller.numTimepointsErrorLabel, FieldType.BYTE), new ValidatableField(controller.numConditionsTextField, controller.numConditionsErrorLabel, FieldType.BYTE), new ValidatableField(controller.numStrainsTextField, controller.numStrainErrorLabel, FieldType.BYTE), new ValidatableField(controller.numReplicatesEmptyInputSheetTextField, controller.numReplicatesErrorLabelEmptyInputSheet, FieldType.BYTE), diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetValidator.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetValidator.java index ba56d78..69b6a4a 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetValidator.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/EmptyInputSheetValidator.java @@ -1,6 +1,7 @@ package org.awdevelopment.smithlab.gui.controllers.main; import org.awdevelopment.smithlab.config.EmptyInputSheetConfig; +import org.awdevelopment.smithlab.config.SampleLabelingType; import org.awdevelopment.smithlab.gui.controllers.AbstractLabelController; import org.awdevelopment.smithlab.gui.controllers.ConditionsController; import org.awdevelopment.smithlab.gui.controllers.StrainsController; @@ -22,6 +23,17 @@ public EmptyInputSheetValidator(EmptyInputSheetFields fields, GUILogger guiLogge this.config = config; } + @Override + public boolean fieldsValid() { + if (!config.usingNumConditions() || config.sampleLabelingType() == SampleLabelingType.STRAIN) { + fields.getNumConditionsTextField().setStatus(FieldStatus.UNUSED); + } + if (!config.usingNumStrains() || config.sampleLabelingType() == SampleLabelingType.CONDITION) { + fields.getNumStrainsTextField().setStatus(FieldStatus.UNUSED); + } + return super.fieldsValid(); + } + void validateNumTimepoints() { validateControllerConnectedField(fields.getNumTimepointsTextField(), fields.getTimepointsController()); } void validateOutputFilename() { validateTextFieldFilename(fields.getOutputFilenameTextField()); } @@ -73,7 +85,9 @@ private void validateControllerConnectedField(ValidatableField field, AbstractLa switch (controller) { case null -> { } case TimepointsController timepointsController -> { - if (timepointsController.usingNumDays()) validateTextFieldByte(field); + if (timepointsController.usingNumDays()) { + validateTextFieldByte(field); + } else { guiLogger.clearError(field.getErrorLabel()); config.setUsingNumDays(timepointsController.usingNumDays()); diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/MainApplicationController.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/MainApplicationController.java index a5e6a6b..8e32b65 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/MainApplicationController.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/MainApplicationController.java @@ -7,6 +7,8 @@ import javafx.scene.layout.HBox; import javafx.stage.Stage; import org.awdevelopment.smithlab.config.*; +import org.awdevelopment.smithlab.data.Condition; +import org.awdevelopment.smithlab.data.Strain; import org.awdevelopment.smithlab.data.experiment.Experiment; import org.awdevelopment.smithlab.gui.FXMLResourceType; import org.awdevelopment.smithlab.gui.SceneLoader; @@ -23,6 +25,8 @@ import org.awdevelopment.smithlab.io.output.OutputGenerator; import org.awdevelopment.smithlab.logging.GUILogger; +import java.util.Set; + public class MainApplicationController extends AbstractController { private ConfigManager config; @@ -108,6 +112,18 @@ public void setup() { emptyInputSheetFields.getSampleLabelingRadioButtons().setStatus(FieldStatus.READY); setupErrorLabelsOutputSheet(); setupErrorLabelsEmptyInputSheet(); + setupSubControllers(); + } + + private void setupSubControllers() { + // Initialize sub-controllers for conditions, strains, and timepoints + conditionsController = new ConditionsController(); + strainsController = new StrainsController(); + timepointsController = new TimepointsController(); + // Set up the sub-controllers + conditionsController.setup(); + strainsController.setup(); + timepointsController.setup(); } private void choiceBoxSetup() { @@ -134,6 +150,11 @@ private void radioButtonSetup() { private void setupErrorLabel(Label label) { label.setText(""); label.setStyle(""); + label.setDisable(false); + label.setVisible(true); + label.setManaged(true); + label.setWrapText(true); + label.setAccessibleText(label.getId()); } private void setupErrorLabels(Label... labels) { @@ -151,7 +172,7 @@ private void setupErrorLabelsEmptyInputSheet() { } public void generateOutput() { - getLogger().atDebug("Mode: " + mode); + getLogger().atFatal("Mode: " + mode); switch (mode) { case GENERATE_OUTPUT_SHEETS -> { guiLogger.clearError(statusLabelOutputSheet); @@ -160,7 +181,10 @@ public void generateOutput() { } case GENERATE_EMPTY_INPUT_SHEET -> { guiLogger.clearError(statusLabelEmptyInputSheet); - if (!emptyInputSheetValidator.fieldsValid()) return; + if (!emptyInputSheetValidator.fieldsValid()) { + return; + } + getLogger().atFatal("Fields are valid, generating empty input sheet..."); generateEmptyInputSheet(); } case IMAGE_RECOGNITION -> {} @@ -247,22 +271,36 @@ public void updateMode() { private void generateEmptyInputSheet() { OutputGenerator outputGenerator; + if (config.getEmptyInputSheetConfig().sampleLabelingType() == SampleLabelingType.STRAIN) { + getLogger().atDebug("Using strain labeling type for empty input sheet."); + } else if (config.getEmptyInputSheetConfig().sampleLabelingType() == SampleLabelingType.CONDITION) { + getLogger().atDebug("Using condition labeling type for empty input sheet."); + } else { + getLogger().atDebug("Using condition and strain labeling type for empty input sheet."); + } + config.getEmptyInputSheetConfig().setUsingNumDays(timepointsController.usingNumDays()); + getLogger().atDebug("CONFIG:\n" + config.getEmptyInputSheetConfig().readableConfig()); try { outputGenerator = new OutputGenerator(config.getEmptyInputSheetConfig()); + getLogger().atDebug("CREATE OUTPUT GENERATOR - SUCCESS"); } catch (NoDaysException | NoStrainsOrConditionsException e) { guiLogger.errorOccurred(statusLabelEmptyInputSheet, e.getMessage()); return; } + getLogger().atDebug("GENERATE EMPTY INPUT SHEET - START"); try { outputGenerator.generateEmptyInputSheet(); + getLogger().atDebug("GENERATE EMPTY INPUT SHEET - SUCCESS"); } catch (OutputException e) { guiLogger.errorOccurred(statusLabelEmptyInputSheet, e.getMessage()); return; } + statusLabelEmptyInputSheet.setText("Successfully generated output!"); statusLabelEmptyInputSheet.setStyle("-fx-text-fill: green"); } + public void updateFieldsEmptyInputSheet() { emptyInputSheetConfigUpdater.updateFields(); emptyInputSheetValidator.preliminaryFieldsValid(); @@ -275,19 +313,28 @@ public void updateFieldsOutputSheets() { public void openConditionsFXML() { updateFieldsEmptyInputSheet(); - conditionsController = (ConditionsController) SceneLoader.loadScene(new Stage(), FXMLResourceType.CONDITIONS, getLogger(), config.getEmptyInputSheetConfig()); + Stage stage = new Stage(); + conditionsController = (ConditionsController) + SceneLoader.loadScene(stage, FXMLResourceType.CONDITIONS, getLogger(), config.getEmptyInputSheetConfig(), this); + conditionsController.loadConditions(config.getEmptyInputSheetConfig().conditions()); } public void openStrainsFXML() { updateFieldsEmptyInputSheet(); - strainsController = (StrainsController) SceneLoader.loadScene(new Stage(), FXMLResourceType.STRAINS, getLogger(), config.getEmptyInputSheetConfig()); + Stage stage = new Stage(); + strainsController = (StrainsController) + SceneLoader.loadScene(stage, FXMLResourceType.STRAINS, getLogger(), config.getEmptyInputSheetConfig(), this); + strainsController.loadStrains(config.getEmptyInputSheetConfig().strains()); } public void openTimepointsFXML() { updateFieldsEmptyInputSheet(); - timepointsController = (TimepointsController) SceneLoader.loadScene(new Stage(), FXMLResourceType.TIMEPOINTS, getLogger(), config.getEmptyInputSheetConfig()); + Stage stage = new Stage(); + timepointsController = (TimepointsController) + SceneLoader.loadScene(stage, FXMLResourceType.TIMEPOINTS, getLogger(), config.getEmptyInputSheetConfig(), this); } + public void updateSampleLabelingRadioButtons(ActionEvent actionEvent) { emptyInputSheetConfigUpdater.updateSampleLabelingRadioButtons(actionEvent); } public void updateNumConditionsEmptyInputSheet(KeyEvent keyEvent) { emptyInputSheetConfigUpdater.updateNumConditions(keyEvent); } @@ -303,4 +350,54 @@ public void openTimepointsFXML() { public void updateOutputFilenameEmptyInputSheet(KeyEvent keyEvent) { emptyInputSheetConfigUpdater.updateOutputFilename(keyEvent); } public Control getControlByID(String id) { return (Control) tabPane.getScene().lookup("#" + id); } + + public void disableNumConditions() { + this.numConditionsErrorLabel.setDisable(true); + this.numConditionsTextField.setDisable(true); + this.emptyInputSheetFields.getNumConditionsTextField().setStatus(FieldStatus.UNUSED); + } + + public void enableNumConditions() { + this.numConditionsErrorLabel.setDisable(false); + this.numConditionsTextField.setDisable(false); + if (this.numConditionsTextField.getText().isEmpty()) { + this.emptyInputSheetFields.getNumConditionsTextField().setStatus(FieldStatus.UNTOUCHED); + } else { + this.emptyInputSheetFields.getNumConditionsTextField().setStatus(FieldStatus.EDITED_NOT_VALIDATED); + } + } + + public void disableNumStrains() { + this.numStrainErrorLabel.setDisable(true); + this.numStrainsTextField.setDisable(true); + this.emptyInputSheetFields.getNumStrainsTextField().setStatus(FieldStatus.UNUSED); + } + + public void enableNumStrains() { + this.numStrainErrorLabel.setDisable(false); + this.numStrainsTextField.setDisable(false); + if (this.numStrainsTextField.getText().isEmpty()) { + this.emptyInputSheetFields.getNumStrainsTextField().setStatus(FieldStatus.UNTOUCHED); + } + else { + this.emptyInputSheetFields.getNumStrainsTextField().setStatus(FieldStatus.EDITED_NOT_VALIDATED); + } + } + + public void disableNumReplicates() { + this.numReplicatesErrorLabelEmptyInputSheet.setDisable(true); + this.numReplicatesEmptyInputSheetTextField.setDisable(true); + this.emptyInputSheetFields.getNumReplicatesTextField().setStatus(FieldStatus.UNUSED); + } + + public void enableNumReplicates() { + this.numReplicatesErrorLabelEmptyInputSheet.setDisable(false); + this.numReplicatesEmptyInputSheetTextField.setDisable(false); + if (this.numReplicatesEmptyInputSheetTextField.getText().isEmpty()) { + this.emptyInputSheetFields.getNumReplicatesTextField().setStatus(FieldStatus.UNTOUCHED); + } else { + this.emptyInputSheetFields.getNumReplicatesTextField().setStatus(FieldStatus.EDITED_NOT_VALIDATED); + } + } + } diff --git a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/validatable_field/FieldStatus.java b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/validatable_field/FieldStatus.java index 6e21037..8e3d003 100644 --- a/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/validatable_field/FieldStatus.java +++ b/src/main/java/org/awdevelopment/smithlab/gui/controllers/main/validatable_field/FieldStatus.java @@ -5,5 +5,6 @@ public enum FieldStatus { INVALID, UNTOUCHED, EMPTY, - EDITED_NOT_VALIDATED + EDITED_NOT_VALIDATED, + UNUSED } diff --git a/src/main/java/org/awdevelopment/smithlab/io/output/OutputGenerator.java b/src/main/java/org/awdevelopment/smithlab/io/output/OutputGenerator.java index 2026ea3..9fa675b 100644 --- a/src/main/java/org/awdevelopment/smithlab/io/output/OutputGenerator.java +++ b/src/main/java/org/awdevelopment/smithlab/io/output/OutputGenerator.java @@ -1,8 +1,10 @@ package org.awdevelopment.smithlab.io.output; -import org.awdevelopment.smithlab.config.ConfigDefaults; import org.awdevelopment.smithlab.config.EmptyInputSheetConfig; import org.awdevelopment.smithlab.config.OutputSheetsConfig; +import org.awdevelopment.smithlab.config.SampleLabelingType; +import org.awdevelopment.smithlab.data.Condition; +import org.awdevelopment.smithlab.data.Strain; import org.awdevelopment.smithlab.data.experiment.EmptyExperiment; import org.awdevelopment.smithlab.io.exceptions.NoDaysException; import org.awdevelopment.smithlab.io.exceptions.NoStrainsOrConditionsException; @@ -12,6 +14,10 @@ import org.awdevelopment.smithlab.logging.LoggerHelper; import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; public class OutputGenerator { @@ -46,10 +52,11 @@ public OutputGenerator(OutputSheetsConfig config) { public OutputGenerator(EmptyInputSheetConfig config) throws NoDaysException, NoStrainsOrConditionsException { LOGGER = config.LOGGER(); - outputFileName = ConfigDefaults.EMPTY_INPUT_SHEET_FILENAME; + outputFileName = config.emptyInputSheetName(); emptyInputSheetName = config.emptyInputSheetName(); includeBaselineColumn = config.includeBaselineColumn(); emptyExperiment = generateEmptyExperiment(config); + LOGGER.atDebug("EMPTY EXPERIMENT:\n" + emptyExperiment.readableExperiment()); GUI = false; // these are not used in this constructor this.outputStyle = null; @@ -79,22 +86,88 @@ public void generateEmptyInputSheet() throws OutputException { else if (this.emptyExperiment.usingNumDays()) { LOGGER.atInfo("No days specified. Using number of days and leaving day numbers blank in empty input sheet."); } + LOGGER.atDebug("WRITING EMPTY INPUT SHEET WITH NAME: " + emptyInputSheetName); emptyInputSheetWriter = new XlsxEmptyInputSheetWriter(emptyInputSheetName, LOGGER, includeBaselineColumn, emptyExperiment); + LOGGER.atDebug("SUCCESSFULLY CREATED EMPTY INPUT SHEET WRITER"); emptyInputSheetWriter.writeEmptyInputSheet(); + LOGGER.atDebug("SUCCESSFULLY WROTE EMPTY INPUT SHEET TO FILE: " + emptyInputSheetName); } private EmptyExperiment generateEmptyExperiment(EmptyInputSheetConfig config) throws NoDaysException { - Byte[] days = config.days().toArray(new Byte[0]); - byte[] daysArray = new byte[days.length]; - for (int i = 0; i < days.length; i++) { daysArray[i] = days[i];} - if (config.usingNumConditions() && config.usingNumStrains()) { - return new EmptyExperiment(config.numStrains(), config.numConditions(), config.numReplicates(), daysArray, config.numDays()); - } else if (config.usingNumConditions()) { - return new EmptyExperiment(config.strains(), config.numConditions(), config.numReplicates(), daysArray, config.numDays()); - } else if (config.usingNumStrains()) { - return new EmptyExperiment(config.numStrains(), config.conditions(), config.numReplicates(), daysArray, config.numDays()); + Set conditionsSet = new LinkedHashSet<>(); + Set strainsSet = new LinkedHashSet<>(); + boolean usingNumConditions, usingNumStrains, usingNumDays; + byte numConditions, numStrains, numReplicates, numDays; + byte[] days; + switch (config.sampleLabelingType()) { + case SampleLabelingType.STRAIN -> { + usingNumConditions = false; + conditionsSet = Collections.emptySet(); + numConditions = -1; // -1 indicates no conditions + usingNumStrains = config.usingNumStrains(); + if (usingNumStrains) { + numStrains = config.numStrains(); + strainsSet = new HashSet<>(); + for (int i = 0; i < numStrains; i++) { strainsSet.add(new Strain("Strain " + (i + 1))); } + } else { + strainsSet = config.strains(); + numStrains = (byte) config.strains().size(); + } + } + case SampleLabelingType.CONDITION -> { + usingNumConditions = config.usingNumConditions(); + strainsSet = Collections.emptySet(); + numStrains = -1; // -1 indicates no strains + usingNumStrains = false; + if (usingNumConditions) { + numConditions = config.numConditions(); + for (int i = 0; i < numConditions; i++) { + conditionsSet.add(new Condition("Condition " + (i + 1))); + } + } else { + conditionsSet = config.conditions(); + numConditions = (byte) config.conditions().size(); + } + } + case SampleLabelingType.CONDITION_AND_STRAIN -> { + usingNumConditions = config.usingNumConditions(); + usingNumStrains = config.usingNumStrains(); + if (usingNumConditions) { + numConditions = config.numConditions(); + for (int i = 0; i < numConditions; i++) { + conditionsSet.add(new Condition("Condition " + (i + 1))); + } + } else { + conditionsSet = config.conditions(); + numConditions = (byte) config.conditions().size(); + } + if (usingNumStrains) { + numStrains = config.numStrains(); + for (int i = 0; i < numStrains; i++) { + strainsSet.add(new Strain("Strain " + (i + 1))); + } + } else { + strainsSet = config.strains(); + numStrains = (byte) config.strains().size(); + } + } + default -> + throw new IllegalArgumentException("Invalid sample labeling type: " + config.sampleLabelingType()); + } + if (config.usingNumDays()) { + usingNumDays = true; + if (config.numDays() <= 0) throw new NoDaysException(); + numDays = config.numDays(); + days = new byte[numDays]; + for (int i = 0; i < numDays; i++) { days[i] = (byte) (i + 1); } } else { - return new EmptyExperiment(config.strains(), config.conditions(), config.numReplicates(), daysArray, config.numDays()); + if (config.days().isEmpty()) throw new NoDaysException(); + Byte[] daysByteArray = config.days().toArray(new Byte[0]); + days = new byte[daysByteArray.length]; + numDays = (byte) days.length; } + numReplicates = config.numReplicates(); + return new EmptyExperiment(conditionsSet, strainsSet, numReplicates, numConditions, numStrains, + usingNumConditions, usingNumStrains, days, numDays, config.usingNumDays()); } } diff --git a/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetHeaderWriter.java b/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetHeaderWriter.java index a6ef6e6..f7dac8a 100644 --- a/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetHeaderWriter.java +++ b/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetHeaderWriter.java @@ -1,6 +1,8 @@ package org.awdevelopment.smithlab.io.output; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; @@ -10,6 +12,7 @@ import org.awdevelopment.smithlab.io.exceptions.NoStrainsOrConditionsException; import org.awdevelopment.smithlab.logging.LoggerHelper; +import java.util.Iterator; import java.util.Set; public class XlsxEmptyInputSheetHeaderWriter { @@ -21,8 +24,9 @@ public class XlsxEmptyInputSheetHeaderWriter { private final boolean usingNumDays; private final boolean includeBaselineColumn; private final byte numDays; + private final byte numReplicates; - public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set conditions, Set strains, byte[] days, boolean includeBaselineColumn) { + public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set conditions, Set strains, byte[] days, boolean includeBaselineColumn, byte numReplicates) { this.LOGGER = logger; this.conditions = conditions; this.strains = strains; @@ -30,9 +34,10 @@ public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set condi this.numDays = (byte) days.length; this.usingNumDays = false; this.includeBaselineColumn = includeBaselineColumn; + this.numReplicates = numReplicates; } - public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set conditions, Set strains, byte numDays, boolean includeBaselineColumn) { + public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set conditions, Set strains, byte numDays, boolean includeBaselineColumn, byte numReplicates) { this.LOGGER = logger; this.conditions = conditions; this.strains = strains; @@ -40,6 +45,7 @@ public XlsxEmptyInputSheetHeaderWriter(LoggerHelper logger, Set condi this.numDays = numDays; this.usingNumDays = true; this.includeBaselineColumn = includeBaselineColumn; + this.numReplicates = numReplicates; } public void generateHeaders(XSSFSheet emptyInputSheet) throws NoStrainsOrConditionsException { @@ -64,6 +70,8 @@ private void generateDayHeaders(XSSFRow headerRow, XSSFRow subHeaderRow, byte la headerRow.createCell(++lastUsedColumn); CellRangeAddress mergeRange = new CellRangeAddress(0, 0, lastUsedColumn - 2, lastUsedColumn); headerRow.getSheet().addMergedRegion(mergeRange); + dayHeaderCell.setCellType(CellType.NUMERIC); + dayHeaderCell.setCellStyle(dayHeaderCellStyle); XSSFCell subHeaderCell = subHeaderRow.createCell(lastUsedColumn - 2); subHeaderCell.setCellValue("Colonies"); subHeaderCell = subHeaderRow.createCell(lastUsedColumn - 1); @@ -81,16 +89,79 @@ private byte generateStrainConditionLabels(XSSFRow headerRow) throws NoStrainsOr } else if (conditions.isEmpty()) { LOGGER.atInfo("No conditions provided - just using strains"); headerRow.createCell(0).setCellValue("Strain"); + generateStrainLabels(headerRow, 0); return 0; } else if (strains.isEmpty()) { LOGGER.atInfo("No strains provided - just using conditions"); headerRow.createCell(0).setCellValue("Condition"); + generateConditionLabels(headerRow, false); return 0; + } else { LOGGER.atInfo("Conditions and strains provided"); headerRow.createCell(0).setCellValue("Condition"); headerRow.createCell(1).setCellValue("Strain"); + generateConditionLabels(headerRow, true); + generateStrainLabels(headerRow, 1); return 1; } } + + private void generateConditionLabels(XSSFRow headerRow, boolean usingStrains) { + int curRowNum = 2; + int mergedAreaHeight; + if (usingStrains) { + mergedAreaHeight = numReplicates * strains.size(); + } else { + mergedAreaHeight = numReplicates; + } + for (Condition condition : conditions) { + LOGGER.atInfo("Generating condition: " + condition); + for (int i = 0; i < mergedAreaHeight; i++) { + headerRow.getSheet().createRow(curRowNum + i).createCell(0); + } + XSSFCell conditionLabelCell = headerRow.getSheet().getRow(curRowNum).getCell(0); + conditionLabelCell.setCellValue(condition.getName()); + // Create a merged cell region that spans the number of replicates for the condition + CellRangeAddress conditionLabelCellAddress = new CellRangeAddress(curRowNum, curRowNum + (mergedAreaHeight - 1), 0, 0); + headerRow.getSheet().addMergedRegion(conditionLabelCellAddress); + curRowNum += mergedAreaHeight; + CellStyle conditionLabelStyle = headerRow.getSheet().getWorkbook().createCellStyle(); + conditionLabelStyle.setWrapText(true); + conditionLabelCell.setCellStyle(conditionLabelStyle); + + } + } + + private void generateStrainLabels(XSSFRow headerRow, int columnIndex) { + int curRowNum = 2; + if (columnIndex == 1) { + for (int i = 0; i < conditions.size(); i++) { + generateOneSetOfStrainLabels(headerRow, columnIndex, curRowNum, numReplicates); + curRowNum += numReplicates * strains.size(); + } + } else { + generateOneSetOfStrainLabels(headerRow, columnIndex, curRowNum, numReplicates); + } + + } + + private void generateOneSetOfStrainLabels(XSSFRow headerRow, int columnIndex, int curRowNum, int numReplicates) { + for (Strain strain : strains) { + // Create a merged cell region that spans the number of replicates for the strain + CellRangeAddress strainLabelCellAddress = new CellRangeAddress(curRowNum, curRowNum + (numReplicates - 1), columnIndex, columnIndex); + headerRow.getSheet().addMergedRegion(strainLabelCellAddress); + for (int i = 0; i < numReplicates; i++) { + if (headerRow.getSheet().getRow(curRowNum + i) == null) { + headerRow.getSheet().createRow(curRowNum + i); + } + } + XSSFCell strainLabelCell = headerRow.getSheet().getRow(curRowNum).createCell(columnIndex); + curRowNum += numReplicates; + strainLabelCell.setCellValue(strain.getName()); + CellStyle strainLabelStyle = headerRow.getSheet().getWorkbook().createCellStyle(); + strainLabelStyle.setWrapText(true); + strainLabelCell.setCellStyle(strainLabelStyle); + } + } } diff --git a/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetWriter.java b/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetWriter.java index f422466..dda6d85 100644 --- a/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetWriter.java +++ b/src/main/java/org/awdevelopment/smithlab/io/output/XlsxEmptyInputSheetWriter.java @@ -41,8 +41,8 @@ public void writeEmptyInputSheet() throws FailedToCreateNewWorkbookException, Wr LOGGER.atInfo("Writing empty input sheet \"" + emptyInputSheetName + "\"..."); try (XSSFWorkbook workbook = new XSSFWorkbook()) { XlsxEmptyInputSheetHeaderWriter headerWriter; - if (usingNumDays) headerWriter = new XlsxEmptyInputSheetHeaderWriter(LOGGER, conditions, strains, numDays, includeBaselineColumn); - else headerWriter = new XlsxEmptyInputSheetHeaderWriter(LOGGER, conditions, strains, days, includeBaselineColumn); + if (usingNumDays) headerWriter = new XlsxEmptyInputSheetHeaderWriter(LOGGER, conditions, strains, numDays, includeBaselineColumn, numReplicates); + else headerWriter = new XlsxEmptyInputSheetHeaderWriter(LOGGER, conditions, strains, days, includeBaselineColumn, numReplicates); headerWriter.generateHeaders(workbook.createSheet()); try { workbook.write(new FileOutputStream(emptyInputSheetName)); diff --git a/src/main/resources/fxml/application.fxml b/src/main/resources/fxml/application.fxml index f4f2cec..43d97f6 100644 --- a/src/main/resources/fxml/application.fxml +++ b/src/main/resources/fxml/application.fxml @@ -108,7 +108,7 @@ - + diff --git a/src/main/resources/fxml/conditions.fxml b/src/main/resources/fxml/conditions.fxml index ae8fbf3..f3efeea 100644 --- a/src/main/resources/fxml/conditions.fxml +++ b/src/main/resources/fxml/conditions.fxml @@ -1,14 +1,45 @@ - - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/fxml/strains.fxml b/src/main/resources/fxml/strains.fxml index 1a292a0..474ec86 100644 --- a/src/main/resources/fxml/strains.fxml +++ b/src/main/resources/fxml/strains.fxml @@ -1,14 +1,44 @@ - - - - - + + + + + + + - + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 226473a..837b40c 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -13,8 +13,8 @@ - - + +