diff --git a/MVC Diagram 2.gif b/MVC Diagram 2.gif new file mode 100644 index 0000000..b5543fd Binary files /dev/null and b/MVC Diagram 2.gif differ diff --git a/MVC diagram.gif b/MVC diagram.gif new file mode 100644 index 0000000..300f5a2 Binary files /dev/null and b/MVC diagram.gif differ diff --git a/pom.xml b/pom.xml index b5c99f2..27ef9fb 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ - Cli + GuiStarter UTF-8 11 11 diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 1d6dfd4..922e5b2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,9 +3,8 @@ module org.baxter.disco.ocr { requires com.pi4j.plugin.raspberrypi; requires com.pi4j.plugin.pigpio; requires com.pi4j.library.pigpio; - //requires javafx.fxml; - //requires javafx.controls; - requires javafx.swing; + requires javafx.fxml; + requires javafx.controls; requires org.apache.poi.poi; requires org.apache.commons.configuration2; requires org.apache.xmlbeans; @@ -13,7 +12,7 @@ module org.baxter.disco.ocr { requires org.bytedeco.leptonica; requires org.bytedeco.opencv; requires org.bytedeco.javacpp; - //requires javafx.graphics; + requires javafx.graphics; requires org.apache.poi.ooxml; requires org.apache.poi.ooxml.schemas; requires org.apache.commons.io; diff --git a/src/main/java/org/baxter/disco/ocr/GuiController.java b/src/main/java/org/baxter/disco/ocr/GuiController.java new file mode 100644 index 0000000..27acefe --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/GuiController.java @@ -0,0 +1,261 @@ +package org.baxter.disco.ocr; + +import java.util.List; + +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; + +/** + * Controller portion of MVC for Accuracy over Life test fixture. + * Mostly wrapper interface between View and Model. + * + * {@link GuiView}, GuiController, and {@link GuiModel} versions are tied together, and are referred to collectively as Gui. + * + * @author Blizzard Finnegan + * @version 0.2.0, 06 Feb, 2023 + */ +public class GuiController +{ + /** + * Wrapper function to get available cameras. + * + * @return List[String] of the names of cameras + */ + public static List getCameras() + { return GuiModel.getCameras(); } + + /** + * Wrapper function used to show an image in a separate window + * + * @param cameraName The camera whose image should be shown. + */ + public static String showImage(String cameraName) + { return GuiModel.showImage(cameraName); } + + /** + * Wrapper function to toggle cropping for a given camera + * + * @param cameraName The camera whose image should be shown. + */ + public static void toggleCrop(String cameraName) + { GuiModel.toggleCrop(cameraName); } + + /** + * Wrapper function to toggle threshold for a given camera + * + * @param cameraName The camera whose image should be shown. + */ + public static void toggleThreshold(String cameraName) + { GuiModel.toggleThreshold(cameraName); } + + /** + * Wrapper function to save the default config + */ + public static void saveDefaults() + { GuiModel.saveDefaults(); } + + /** + * Wrapper function to save the current config + */ + public static void save() + { GuiModel.save(); } + + /** + * Wrapper function to save the current config, and re-enable image processing if necessary. + */ + public static void saveClose() + { GuiModel.save(); GuiModel.enableProcessing(); } + + /** + * Wrapper function to get a config value, for a given camera + * + * @param cameraName The name of the camera being inspected + * @param property The config property to be returned + * + * @return String of the value of the current object + */ + public static String getConfigString(String cameraName, ConfigProperties property) + { return GuiModel.getConfigString(cameraName,property); } + + /** + * Wrapper function to get a config value, for a given camera + * + * @param cameraName The name of the camera being inspected + * @param property The config property to be returned + * + * @return String of the value of the current object + */ + public static double getConfigValue(String cameraName, ConfigProperties property) + { return GuiModel.getConfigValue(cameraName,property); } + + /** + * Wrapper function to set a config value for a given camera. + * + * @param cameraName The name of the camera being modified + * @param property The property to be modified + * @param value The new value to set the property to + */ + public static void setConfigValue(String cameraName, ConfigProperties property, double value) + { + GuiModel.setConfigVal(cameraName,property,value); + if(property == ConfigProperties.CROP_W) + GuiView.updateImageViewWidth(cameraName); + if(property == ConfigProperties.CROP_H) + GuiView.updateImageViewHeight(cameraName); + } + + /** + * Setter for the number of iterations + * + * @param iterationCount The new iteration count to be saved + */ + public static void setIterationCount(int iterationCount) + { GuiModel.setIterations(iterationCount); } + + /** + * Wrapper function to interrupt testing. + */ + public static void interruptTests() + { GuiModel.interruptTesting(); } + + /** + * Wrapper function to run tests. + */ + public static void runTests() + { GuiModel.runTests(); } + + /** + * Wrapper function to test the movement of the fixture. + */ + public static void testMotions() + { + testingMotions(); + GuiModel.testMovement(); + } + + /** + * If the Model is ready, set the view to allow the Start button to be pressed. + */ + public static void updateStart() + { + boolean ready = GuiModel.isReady(); + GuiView.getStart().setDisable(ready); + if(ready) GuiView.getStart().setTooltip(new Tooltip("Start running automated testing.")); + } + + /** + * Wrapper function to write to the user feedback Label, stating that the test is starting. + */ + public static void startTests() + { userUpdate("Starting tests..."); } + + /** + * Updates the View's state wit hthe current iteration count + */ + public static void updateIterations() + { + String newIterations = Integer.toString(GuiModel.getIterations()); + GuiView.getIterationField().setText(newIterations); + } + + /** + * Update a given config value, given a camera + * + * @param cameraName The name of the camera being updated + * @param property The property being updated + */ + public static void updateConfigValue(String cameraName, ConfigProperties property) + { + TextField field = GuiView.getField(cameraName,property); + field.setText(GuiModel.getConfigString(cameraName,property)); + field.setPromptText(GuiModel.getConfigString(cameraName,property)); + } + + /** + * Wrapper function to write the current iteration ot the user feedback Label. + * + * @param index The current iteration number + */ + public static void runningUpdate(int index) + { userUpdate("Running iteration " + index + "..."); } + + /** + * Wrapper function used to set a custom message to the user. + * + * @param output What should be sent to the user. + */ + public static void userUpdate(String output) + { GuiView.getFeedbackText().setText(output); } + + /** + * Wrapper function to tell the user that fixture movement testing has begun. + */ + public static void testingMotions() + { userUpdate("Testing fixture movement..."); } + + /** + * Wrapper function to tell the user that fixture movement testing was successful. + */ + public static void testingMotionSuccessful() + { userUpdate("Fixture movement test successful!"); } + + /** + * Wrapper function to tell the user that fixture movement testing failed, and where it failed. + * + * @param failurePoint Where the movement failed. + */ + public static void testingMotionUnsuccessful(String failurePoint) + { userUpdate("Fixture movement unsuccessful! Fail point: " + failurePoint);} + + /** + * Wrapper function for the Model's pressButton function. + */ + public static void pressButton() + { GuiModel.pressButton(); } + + /** + * Wrapper function used to update whether or not the DUTs should be primed before testing. + */ + public static void updatePrime() + { GuiModel.updatePrime(); } + + /** + * Wrapper function to set the Serial for a given camera. + * + * @param cameraName The name of the camera to modify + * @param serial The serial of the DUT under the given camera + */ + public static void setSerial(String cameraName, String serial) + { GuiModel.setSerial(cameraName,serial); } + + /** + * Getter for the current iteration count + * + * @return String of the current iteration count. + */ + public static String getIterationCount() + { return Integer.toString(GuiModel.getIterations()); } + + /** + * Wrapper function around the GuiModel's calibrateCameras function + */ + public static void calibrateCameras() + { GuiModel.calibrateCameras(); } + + /** + * Close function for the Model; used to end the program + */ + public static void closeModel() { GuiModel.close(); } + + /** + * Function used to update the ImageView of the GUID + * + * @param cameraName Name of the camera the image is from + * @param fileURL The URL of the file to be shown + */ + public static void updateImage(String cameraName, String fileURL) + { + GuiView.getViewMap().get(cameraName).setImage(new Image(fileURL)); + } +} diff --git a/src/main/java/org/baxter/disco/ocr/GuiModel.java b/src/main/java/org/baxter/disco/ocr/GuiModel.java new file mode 100644 index 0000000..83a5331 --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/GuiModel.java @@ -0,0 +1,312 @@ +package org.baxter.disco.ocr; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Model portion of MVC for the Accuracy Over Life test fixture. + * Primarily a wrapper around other classes, but does store some information. + * + * {@link GuiView}, {@link GuiController}, and GuiModel versions are tied together, and are referred to collectively as Gui. + * + * @author Blizzard Finnegan + * @version 0.2.0, 06 Feb, 2023 + */ +public class GuiModel +{ + /** + * Whether or not the backend is prepared to start running. + */ + private static boolean readyToRun = false; + + /** + * The number of iterations. + */ + private static int iterationCount = 3; + + /** + * The Lock object, used for multithreading of the testing function + */ + public static final Lock LOCK = new ReentrantLock(); + + /** + * The testing thread object + */ + private static Thread testingThread = new Thread(); + + /** + * The Movement Facade instance + */ + private static final MovementFacade fixture = new MovementFacade(LOCK); + + /** + * The function called to define the GUI as ready to start testing. + */ + public static void ready() { readyToRun = true; GuiController.updateStart(); } + + /** + * Getter for {@link #readyToRun} + * + * @return boolean of whether or not testing can be started + */ + public static boolean isReady() { return readyToRun; } + + /** + * Setter for the number of iterations + * + * @param iterations The number of times to run the tests + */ + public static void setIterations(int iterations) + { + iterationCount = iterations; + GuiController.userUpdate("Iterations set to: " + iterationCount); + GuiController.updateIterations(); + } + + /** + * Getter for the number of iterations + * + * @return int of the number of iterations to be perfomed. + */ + public static int getIterations() { return iterationCount; } + + /** + * Wrapper around the MovementFacade's testMotions function. + * + * Updates the GUI with whether the testing was successful. + */ + public static void testMovement() + { + GuiController.testingMotions(); + boolean success = fixture.testMotions(); + if(success) GuiController.testingMotionSuccessful(); + else GuiController.testingMotionUnsuccessful("Unknown"); + } + + /** + * Getter for the list of cameras. + * + * @return List[String] of camera names. + */ + public static List getCameras() + { return new ArrayList<>(OpenCVFacade.getCameraNames()); } + + /** + * Function that calls the OpenCVFacade image generation function. + * @return String url of the location of the new image + */ + public static String showImage(String cameraName) + { + return OpenCVFacade.showImage(cameraName, new Object()); + } + + /** + * Setter for a given camera's config value + * + * @param cameraName Name of the camera to be configured + * @param property Property to be changed + * @param value New value for the given property + */ + public static void setConfigVal(String cameraName, ConfigProperties property, double value) + { + ConfigFacade.setValue(cameraName,property,value); + GuiController.updateConfigValue(cameraName,property); + } + + /** + * Getter for a given camera's config value, in String format + * + * @param cameraName Name of the camera to get the config value from + * @param property Property to get the value of + * + * @return String of the current value in the config + */ + public static String getConfigString(String cameraName, ConfigProperties property) + { return Double.toString(ConfigFacade.getValue(cameraName,property)); } + + /** + * Getter for a given camera's config value + * + * @param cameraName Name of the camera to get the config value from + * @param property Property to get the value of + * + * @return double of the current value in the config + */ + public static double getConfigValue(String cameraName, ConfigProperties property) + { return ConfigFacade.getValue(cameraName,property); } + + /** + * Wrapper function around the MovementFacade's pressButton function. + */ + public static void pressButton() + { fixture.pressButton(); } + + /** + * Function used to update whether or not cameras should be primed. + */ + public static void updatePrime() + { + for(String cameraName : OpenCVFacade.getCameraNames()) + { + boolean old = (ConfigFacade.getValue(cameraName,ConfigProperties.PRIME) == 0.0 ); + ConfigFacade.setValue(cameraName,ConfigProperties.PRIME,(old ? 1 : 0)); + } + } + + /** + * Wrapper function to enable all image processing. + */ + public static void enableProcessing() + { + for(String camera : getCameras()) + { + ConfigFacade.setValue(camera,ConfigProperties.CROP, 1.0); + ConfigFacade.setValue(camera,ConfigProperties.THRESHOLD, 1.0); + } + } + + /** + * Wrapper function to save the default config values. + */ + public static void saveDefaults() + { ConfigFacade.saveDefaultConfig(); } + + /** + * Save the current config, and ensure it is loaded properly. + */ + public static void save() + { ConfigFacade.saveCurrentConfig(); ConfigFacade.loadConfig(); } + + /** + * Toggles the threshold processing for the given camera. + * + * @param cameraName The name of the camera to be modified + */ + public static void toggleThreshold(String cameraName) + { + boolean old = (ConfigFacade.getValue(cameraName,ConfigProperties.PRIME) == 0.0 ); + ConfigFacade.setValue(cameraName,ConfigProperties.THRESHOLD,(old ? 1 : 0)); + } + + /** + * Toggles the cropping of the image for the given camera. + * + * @param cameraName The name of the camera to be modified + */ + public static void toggleCrop(String cameraName) + { + boolean old = (ConfigFacade.getValue(cameraName,ConfigProperties.PRIME) == 0.0 ); + ConfigFacade.setValue(cameraName,ConfigProperties.CROP,(old ? 1 : 0)); + } + + /** + * Function used to run all tests. + * + * Currently not working. Will need to rewrite. + */ + public static void runTests() + { + //testingThread = new Thread(() -> + //{ + GuiController.startTests(); + DataSaving.initWorkbook(ConfigFacade.getOutputSaveLocation(),OpenCVFacade.getCameraNames().size()); + boolean prime = false; + List cameraList = new ArrayList<>(); + for(String cameraName : OpenCVFacade.getCameraNames()) + { + if(ConfigFacade.getValue(cameraName,ConfigProperties.PRIME) != 0) + { + prime = true; + } + cameraList.add(cameraName); + } + fixture.iterationMovement(prime); + fixture.pressButton(); + fixture.iterationMovement(prime); + Map resultMap = new HashMap<>(); + Map cameraToFile = new HashMap<>(); + for(String cameraName : cameraList) + { + cameraToFile.put(cameraName,new File("/dev/null")); + } + for(int i = 0; i < iterationCount; i++) + { + while(!LOCK.tryLock()) {} + fixture.iterationMovement(prime); + LOCK.unlock(); + for(String cameraName : cameraList) + { + while(!LOCK.tryLock()) {} + File file = OpenCVFacade.completeProcess(cameraName); + GuiController.updateImage(cameraName,file.getPath()); + LOCK.unlock(); + while(!LOCK.tryLock()) {} + cameraToFile.replace(cameraName,file); + LOCK.unlock(); + } + LOCK.unlock(); + for(String cameraName : cameraList) + { + while(!LOCK.tryLock()) {} + File file = cameraToFile.get(cameraName); + LOCK.unlock(); + while(!LOCK.tryLock()) {} + Double result = TesseractFacade.imageToDouble(file); + LOCK.unlock(); + while(!LOCK.tryLock()) {} + resultMap.put(file,result); + LOCK.unlock(); + while(!LOCK.tryLock()) {} + ErrorLogging.logError("DEBUG: Tesseract final output: " + result); + LOCK.unlock(); + } + while(!LOCK.tryLock()) {} + DataSaving.writeValues(i,resultMap,cameraToFile); + LOCK.unlock(); + GuiController.runningUpdate(i); + } + //println("======================================="); + ErrorLogging.logError("Testing complete!"); + //}); + //testingThread.run(); + } + + /** + * Wrapper function to close everything. + */ + public static void close() + { + ErrorLogging.logError("DEBUG: PROGRAM CLOSING."); + fixture.closeGPIO(); + ErrorLogging.logError("DEBUG: END OF PROGRAM."); + ErrorLogging.closeLogs(); + } + + /** + * Function used to interrupt the testing thread. + * + * As of Gui 0.2.0, this does not work properly. + */ + public static void interruptTesting() { testingThread.interrupt(); } + + /** + * Function to set the serial number for a given camera + * + * @param cameraName name of the camera to be modified + * @param serial serial number to be set + */ + public static void setSerial(String cameraName, String serial) + { ConfigFacade.setSerial(cameraName,serial); } + + /** + * Function to force fixture down before starting to calibrate the cameras. + */ + public static void calibrateCameras() + { fixture.goDown(); } + +} diff --git a/src/main/java/org/baxter/disco/ocr/GuiStarter.java b/src/main/java/org/baxter/disco/ocr/GuiStarter.java new file mode 100644 index 0000000..2309f9e --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/GuiStarter.java @@ -0,0 +1,16 @@ +package org.baxter.disco.ocr; + +/** + * Wrapper class around GuiView. + * + * Maven will not build the {@link GuiView} properly, since it inherits from {@link javafx.application.Application}. + * This will start the Gui's main function, with no other functionality. + * + * @author Blizzard Finnegan + * @version 1.0.1, 01 Feb. 2023 + */ +public class GuiStarter +{ + public static void main(String[] args) + { GuiView.main(args); } +} diff --git a/src/main/java/org/baxter/disco/ocr/GuiView.java b/src/main/java/org/baxter/disco/ocr/GuiView.java new file mode 100644 index 0000000..601510c --- /dev/null +++ b/src/main/java/org/baxter/disco/ocr/GuiView.java @@ -0,0 +1,916 @@ +package org.baxter.disco.ocr; + +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; + +import javafx.application.Application; +import javafx.geometry.*; +import javafx.scene.*; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.text.*; +import javafx.stage.Stage; + +/** + * View portion of MVC for the Accuracy Over Life test fixture. + * + * GuiView, {@link GuiController}, and {@link GuiModel} versions are tied together, and are referred to collectively as Gui. + * + * @author Blizzard Finnegan + * @version 0.2.0, 06 Feb, 2023 + */ +public class GuiView extends Application +{ + /** + * Scene used for the Main Menu. + */ + public static final Scene MAIN_MENU; + + /** + * The base Node object for the Main menu; used to define window borders. + */ + private static final AnchorPane MAIN_ANCHOR; + + /** + * The Node object within the {@link #MAIN_ANCHOR}, where all portions of the main menu are stored. + */ + private static final Pane MAIN_PANE; + + + /** + * Scene used for the camera configuration menu + */ + public static final Scene CAMERA_MENU; + + /** + * The base Node object for the camera config menu; used to define window borders. + */ + private static final AnchorPane CAMERA_ANCHOR; + + /** + * The node object within the {@link #CAMERA_ANCHOR}, where all portions of the camera config menu are stored. + */ + private static final Pane CAMERA_PANE; + + /** + * An easily-accessible map of text fields used in the Camera config menu. + * The outer map's keys are the respective cameras, with the inner map being config properties and the associated text field. + */ + private static final Map> uiFields = new HashMap<>(); + + /** + * An easily-accessible location for the user-feedback Text object. + */ + private static Text userFeedback; + + /** + * An easily accessible location for the TextField the user uses to set the current amount of iterations. + */ + private static TextField iterationField; + + /** + * Easily-accessible Button object for the start button. + */ + private static Button startButton; + + /* + * Easily-accessible Button object for the stop button. + */ + //private static Button stopButton; + + /** + * The main Stage object, used in the GUI. + * + * The Stage object is analogous to the window generated. + */ + private static Stage STAGE; + + /** + * Value used for spacing within VBoxes and HBoxes + */ + private static double INTERNAL_SPACING = 5.0; + + /** + * Value used for window border spacing + */ + private static double EXTERNAL_SPACING = 10.0; + + /** + * Map used to store ImageViews + */ + private static Map viewMap = new HashMap<>(); + + /** + * The wrapper function to spawn a new JavaFX Stage. + */ + public static void main(String[] args) { launch(args); } + + /** + * Initialiser for the static objects. + */ + static + { + ErrorLogging.logError("START OF PROGRAM"); + ErrorLogging.logError("Setting up main menu..."); + MAIN_ANCHOR = new AnchorPane(); + MAIN_ANCHOR.setMinWidth(Double.NEGATIVE_INFINITY); + MAIN_ANCHOR.setMinHeight(Double.NEGATIVE_INFINITY); + MAIN_PANE = new Pane(); + + //Set the window border + AnchorPane.setTopAnchor(MAIN_PANE,EXTERNAL_SPACING); + AnchorPane.setLeftAnchor(MAIN_PANE,EXTERNAL_SPACING); + AnchorPane.setRightAnchor(MAIN_PANE,EXTERNAL_SPACING); + AnchorPane.setBottomAnchor(MAIN_PANE,EXTERNAL_SPACING); + MAIN_ANCHOR.getChildren().add(MAIN_PANE); + MAIN_MENU = new Scene(MAIN_ANCHOR); + + ErrorLogging.logError("Setting up camera config menu..."); + CAMERA_ANCHOR = new AnchorPane(); + CAMERA_ANCHOR.setMinWidth(Double.NEGATIVE_INFINITY); + CAMERA_ANCHOR.setMinHeight(Double.NEGATIVE_INFINITY); + CAMERA_PANE = new Pane(); + + //Set the window border + AnchorPane.setTopAnchor(CAMERA_PANE,EXTERNAL_SPACING); + AnchorPane.setLeftAnchor(CAMERA_PANE,EXTERNAL_SPACING); + AnchorPane.setRightAnchor(CAMERA_PANE,EXTERNAL_SPACING); + AnchorPane.setBottomAnchor(CAMERA_PANE,EXTERNAL_SPACING); + CAMERA_ANCHOR.getChildren().add(CAMERA_PANE); + CAMERA_MENU = new Scene(CAMERA_ANCHOR); + + //Initialise the camera fields map and imageview map + for(String camera : GuiModel.getCameras()) + { + uiFields.put(camera, new HashMap<>()); + ImageView view = new ImageView(); + view.setId(camera + "-view"); + view.setFitWidth(GuiController.getConfigValue(camera,ConfigProperties.CROP_W)); + view.setFitHeight(GuiController.getConfigValue(camera,ConfigProperties.CROP_H)); + viewMap.put(camera, view); + } + + STAGE.setOnCloseRequest( (event) -> GuiController.closeModel() ); + } + + @Override + public void start(Stage stage) throws Exception + { + ErrorLogging.logError("Finalising GUI..."); + STAGE = stage; + mainMenuBuilder(); + cameraMenuBuilder(); + STAGE.setScene(MAIN_MENU); + STAGE.show(); + ErrorLogging.logError("Gui loading complete."); + } + + /** + * Camera Configuration Menu builder function. + * + * Creates a {@link VBox}, creates a {@link #cameraSetup(String)} object, and adds a separator between each camera. + * Finally, sets the created VBox to be the child of the {@link #CAMERA_PANE}, so it can be shown. + */ + private static void cameraMenuBuilder() + { + VBox layout = new VBox(); + layout.setSpacing(INTERNAL_SPACING); + layout.setAlignment(Pos.CENTER_LEFT); + + int index = 0; + for(String cameraName : GuiModel.getCameras()) + { + if(index != 0) layout.getChildren().add(new Separator(Orientation.HORIZONTAL)); + layout.getChildren().add(cameraSection(cameraName)); + index++; + } + layout.getChildren().add(cameraMenuButtons()); + CAMERA_PANE.getChildren().add(layout); + } + + /** + * Main Menu builder function. + * + * Creates a VBox, fills it with the {@link #topHalf()}, a {@link Separator}, and the {@link #bottomHalf()} + * Finally, sets the created VBox to be the child of the {@link #MAIN_PANE}, so it can be shown. + */ + private static void mainMenuBuilder() + { + VBox layout = new VBox(); + layout.getChildren().addAll(topHalf(), + new Separator(Orientation.HORIZONTAL), + bottomHalf()); + MAIN_PANE.getChildren().add(layout); + } + + + /** + * Builder for the top half of the main menu. + * + * Creates a VBox, fills it with the {@link #topButtons()}, a {@link Separator}, the {@link #setupSection()}, + * the {@link #primeCheckbox()}, and the {@link #testFeedback()}. + * + * @return VBox described above + */ + private static VBox topHalf() + { + VBox output = new VBox(); + output.setSpacing(INTERNAL_SPACING); + output.getChildren().addAll(topButtons(), + new Separator(Orientation.HORIZONTAL), + setupSection(), + primeCheckbox(), + testFeedback()); + return output; + } + + /** + * Builder for the priming section of the main menu. + * + * Builds a pre-defined checkbox for setting whether the DUTs should be primed. + * + * @return CheckBox with a preset Tooltip, Id, and Listener. + */ + private static CheckBox primeCheckbox() + { + CheckBox output = new CheckBox("Prime devices"); + output.setTooltip(new Tooltip("This presses the button on the device under test twice for every iteration.")); + output.setSelected(true); + output.setId("primeCheckbox"); + output.selectedProperty().addListener( + (obeservableValue, oldValue, newValue) -> + { + GuiController.updatePrime(); + }); + return output; + } + + /** + * Builder for the user feedback section of the main menu. + * + * Creates an HBox, fills it with a {@link Label} and a {@link Text} used for communicating + * program status. + * + * @return HBox defined above + */ + private static HBox testFeedback() + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + Label textboxLabel = new Label("Test feedback: "); + Text textbox = new Text("Awaiting input..."); + userFeedback = textbox; + textbox.setId("testOutputToUser"); + + output.getChildren().addAll(textboxLabel,textbox); + return output; + } + + /** + * Builder function for the iteration count user input. + * + * Creates an HBox, filled with a Label and a TextField for user input. + * This TextField is used for setting the number of iterations to complete. + * + * @return HBox defined above + */ + private static HBox setupSection() + { + HBox output = userTextField("Cycles:",GuiController.getIterationCount(), "Enter the number of times to test the devices in the fixture."); + TextField field = null; + for(Node child : output.getChildren()) + { + if(child instanceof TextField) + { + field = (TextField)child; + break; + } + } + if(field == null) + { + ErrorLogging.logError("GUI INIT ERROR!!! - Failed text field setup."); + GuiController.closeModel(); + } + + iterationField = field; + //TextField textField = (TextField)(output.lookup("#cycles")); + field.textProperty().addListener( + (observable, oldValue, newValue) -> + { + try(Scanner sc = new Scanner(newValue);) + { GuiController.setIterationCount(sc.nextInt()); } + catch(Exception e) + { + ErrorLogging.logError("USER INPUT ERROR: Illegal input in cycles count."); + newValue = oldValue; + } + }); + + return output; + } + + /** + * Builder function for the top buttons of the Main Menu. + * + * Creates an HBox for the top buttons, then fills it with a + * - start Button + * - Runs the tests. As of Gui 0.2.0, this only partially runs the first portion of the test. + * - stop Button + * - Intended to stop the test. As of Gui 0.2.0, this has not yet been properly implemented. + * - calibrate cameras Button + * - Changes Scene to {@link #CAMERA_MENU}, allowing for camera setup. + * - test movement Button + * - Tests the movement of the fixture, informs the user of the test's success/failure + * - close Button + * - Closes the window, and the program. Note that as of Gui 0.2.0, this errors out the JVM + * + * @return HBox containing the above-listed buttons. + */ + private static HBox topButtons() + { + //Initial HBox creation + HBox topButtons = new HBox(); + topButtons.setSpacing(INTERNAL_SPACING); + topButtons.setAlignment(Pos.CENTER); + topButtons.setMinWidth(Region.USE_COMPUTED_SIZE); + topButtons.setMinHeight(Region.USE_COMPUTED_SIZE); + + //Start button creation + final Button START = buttonBuilder("Start",true); + startButton = START; + + //Stop button created early, as it is affected by Start, and must be passed in + final Button STOP = buttonBuilder("Stop",true); + + //Start button action and tooltip setting. + START.setOnAction( (event) -> + { + START.setDisable(true); + STOP.setDisable(false); + GuiController.runTests(); + }); + START.setTooltip(new Tooltip("Configure cameras to start the program.")); + + //Stop button action and tooltip setting. + STOP.setOnAction( (event) -> + { + GuiController.interruptTests(); + START.setDisable(false); + STOP.setDisable(true); + }); + STOP.setTooltip(new Tooltip("Pauses current iteration.")); + + //Calibrate Cameras button creation + Button calibrateCamera = buttonBuilder("Calibrate Cameras",false); + calibrateCamera.setOnAction( + (event) -> + { + GuiController.calibrateCameras(); + STAGE.setScene(CAMERA_MENU); + }); + + + //Test Movement button creation + Button testMovement = buttonBuilder("Test Movement",false); + testMovement.setOnAction( (event) -> GuiController.testMotions() ); + + //Close button creation + Button cancel = buttonBuilder("Close",false); + cancel.setOnAction( (event) -> + { + GuiController.closeModel(); + STAGE.close(); + }); + + + //Put the above buttons into the HBox + topButtons.getChildren().addAll(START, + STOP, + calibrateCamera, + testMovement, + cancel); + return topButtons; + } + + /** + * Builder function for the bottom half of the main menu. + * + * Creates an HBox, with however many cameras exist, and their associated {@link #camera(String)} views. + * + * @return Hbox described above + */ + private static HBox bottomHalf() + { + HBox output = new HBox(); + output.setAlignment(Pos.CENTER); + output.setSpacing(INTERNAL_SPACING); + + int index = 0; + for(String camera : GuiModel.getCameras()) + { + if(index != 0) output.getChildren().add(new Separator(Orientation.VERTICAL)); + output.getChildren().add(camera(camera)); + index++; + } + return output; + } + + /** + * Builder for a camera view for the main menu. + * + * Creates a VBox, containing: + * - {@link #cameraHeader(String)} + * - HBox with a Label and TextField for the user to set a DUT's serial number. + * - {@link #cameraView(String)} + * + * @param cameraName The name of the camera to be attached to. + * + * @return VBox described above + */ + private static VBox camera(String cameraName) + { + VBox output = new VBox(); + output.setAlignment(Pos.CENTER_LEFT); + output.setSpacing(INTERNAL_SPACING); + + HBox serialNumber = userTextField("DUT Serial Number:","","Enter the serial number for the device under test."); + + TextField field = null; + for(Node child : serialNumber.getChildren()) + { + if(child instanceof TextField) + { + field = (TextField)child; + break; + } + } + + field.setId("serial" + cameraName); + field.textProperty().addListener( + (observable, oldValue, newValue) -> GuiController.setSerial(cameraName, newValue)); + + output.getChildren().addAll(cameraHeader(cameraName), + serialNumber, + cameraView(cameraName)); + return output; + } + + /** + * Builder for the camera header, for the main menu. + * + * Creates an HBox, containing a Label with the camera's name, and a checkbox to mark whether it is active. + * As of Gui 0.2.0, the checkbox does not work properly. + * + * @param cameraName The name of the camera being accessed. + * + * @return HBox described above. + */ + private static HBox cameraHeader(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER); + Label label = new Label("Camera: " + cameraName); + CheckBox checkBox = new CheckBox("Active"); + checkBox.setSelected(true); + checkBox.setOnAction( (event) -> {/*implement*/}); + checkBox.setId(cameraName.toLowerCase()); + output.getChildren().addAll(label, + checkBox); + return output; + } + + /** + * Builder for the camera view, used in the main menu. + * + * Creates an HBox, containing: + * - A Label for defining what the following label means (OCR Read:) + * - A Label for showing what the OCR reading is. + * - As of Gui 0.2.0, this has not been implemented + * - An ImageView object for showing the final image + * - As of Gui 0.2.0, this has not been implemented + * @param cameraName Name of the camera being accessed + * + * @return HBox described above. + */ + private static HBox cameraView(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + Label label = new Label("OCR Read:"); + Label ocrRead = new Label("[ ]"); + ocrRead.setId("cameraOCR-" + cameraName); + ImageView imageView = viewMap.get(cameraName); + //imageView.setImage(new Image(GuiController.showImage(cameraName))); + output.getChildren().addAll(label, + ocrRead, + imageView); + return output; + } + + /** + * Builder function for the user-editable section in the camera config menu. + * + * Creates a VBox, containing: + * - A Label (used for a section header) + * - A series of CheckBoxes used to define whether to crop and/or threshold the image + * - An HBox of inputs, used to define cropping values. (Defined by {@link #cropInputs(String)}) + * - An HBox of inputs, used to define the threshold value, and how many images to compose together (Defined by {@link #miscInputs(String)}) + * + * @param cameraName The name of the camera being modified + * + * @return The VBox described above + */ + private static VBox cameraSetup(String cameraName) + { + VBox output = new VBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + Label sectionHeader = new Label("Camera: " + cameraName); + output.getChildren().addAll(sectionHeader, + processingInputs(cameraName), + cropInputs(cameraName), + miscInputs(cameraName)); + return output; + } + + /** + * Builder function for a complete section in the camera config menu. + * + * Creates an HBox, containing: + * - A VBox, created by {@link #cameraSetup(String)} + * - An ImageView, which will be used to show the image to the user + */ + private static HBox cameraSection(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + output.getChildren().add(cameraSetup(cameraName)); + + ImageView imageView = viewMap.get(cameraName); + output.getChildren().add(imageView); + + return output; + } + + /** + * Builder for the processing section of the {@link #cameraSetup(String)}, used in the Camera Config section. + * + * Creates an HBox containing: + * - A Button for creating a temporary preview + * - A CheckBox to toggle the cropping of the image + * - A CheckBox to toggle the thresholding of the image + * + * @param cameraName The name of the camera being modified + * + * @return HBox, as described above + */ + private static HBox processingInputs(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + //Preview button generation + Button preview = buttonBuilder("Preview"); + preview.setId("previewButton-" + cameraName); + preview.setOnAction( (event) -> + { + GuiController.pressButton(); + try{ Thread.sleep(2000); } catch(Exception e){ ErrorLogging.logError(e); } + String imageURL = GuiController.showImage(cameraName); + viewMap.get(cameraName).setImage(new Image(imageURL)); + }); + + //Crop image toggle checkbox creation + CheckBox cropPreview = new CheckBox("Crop preview"); + cropPreview.setSelected(true); + cropPreview.setId("cropToggle-" + cameraName); + cropPreview.selectedProperty().addListener((obeservableValue, oldValue, newValue) -> + GuiController.toggleCrop(cameraName)); + cropPreview.setOnAction( (event) -> GuiController.toggleCrop(cameraName) ); + + //Threshold image toggle switch creation + CheckBox thresholdPreview = new CheckBox("Threshold preview"); + thresholdPreview.setSelected(true); + thresholdPreview.selectedProperty().addListener((obeservableValue, oldValue, newValue) -> + GuiController.toggleThreshold(cameraName)); + thresholdPreview.setId("thresholdToggle-" + cameraName); + thresholdPreview.setOnAction( (event) -> GuiController.toggleThreshold(cameraName) ); + + + output.getChildren().addAll(preview, + cropPreview, + thresholdPreview); + return output; + } + + /** + * Builder function for the crop values, stored within a {@link #cameraSetup(String)} in the Camera Config menu. + * + * Creates an HBox, containing: + * - A Label and TextField for each of the following: + * - Crop X + * - Crop Y + * - Crop Width + * - Crop Height + * + * @param cameraName The name of the camera being modified + * + * @return HBox, as defined above + */ + private static HBox cropInputs(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + HBox cropX = userTextField("X:", + GuiController.getConfigString(cameraName,ConfigProperties.CROP_X), + "X-value of the top left corner of the newly cropped image. Only accepts whole numbers."); + textFieldSetup(cropX,ConfigProperties.CROP_X,cameraName); + + HBox cropY = userTextField("Y:", + GuiController.getConfigString(cameraName,ConfigProperties.CROP_Y), + "Y-value of the top left corner of the newly cropped image. Only accepts whole numbers."); + textFieldSetup(cropY,ConfigProperties.CROP_Y,cameraName); + + HBox cropW = userTextField("Width:", + GuiController.getConfigString(cameraName,ConfigProperties.CROP_W), + "Width, in pixels, of the newly cropped image. Only accepts whole numbers."); + textFieldSetup(cropW,ConfigProperties.CROP_W,cameraName); + + HBox cropH = userTextField("Height:", + GuiController.getConfigString(cameraName,ConfigProperties.CROP_H), + "Height, in pixels, of the newly cropped image. Only accepts whole numbers."); + textFieldSetup(cropH, ConfigProperties.CROP_H, cameraName); + + output.getChildren().addAll(cropX, + cropY, + cropW, + cropH); + return output; + } + + /** + * Builder function for the other modifiable values for the {@link #cameraSetup(String)} portion of the camera config menu. + * + * Creates an HBox, containing a Label and TextField for: + * - threshold value + * - number of composite frames + * + * @param cameraName The name of the camera being configured + * + * @return HBox, defined above + */ + private static HBox miscInputs(String cameraName) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + + HBox thresholdValue = userTextField("Threshold Value:", + GuiController.getConfigString(cameraName,ConfigProperties.THRESHOLD), + "This value can be set from 0 to 255. Higher values mean more black in "+ + "the thresholded image. For more information, see the documentation."); + textFieldSetup(thresholdValue,ConfigProperties.THRESHOLD_VALUE,cameraName); + + HBox compositeFrames = userTextField("Composite Frames:", + GuiController.getConfigString(cameraName,ConfigProperties.COMPOSITE_FRAMES), + "Number of frames to bitwise-and together."); + textFieldSetup(compositeFrames,ConfigProperties.COMPOSITE_FRAMES,cameraName); + + output.getChildren().addAll(thresholdValue, + compositeFrames); + return output; + } + + /** + * Builder function for the final buttons in the Camera Config menu. + * + * Creates an HBox, containing: + * - Save Defaults button + * - Save Current button + * - Save and Close button + * - Close without Saving button + * + * @return HBox, as described above + */ + private static HBox cameraMenuButtons() + { + HBox output = new HBox(); + output.setAlignment(Pos.CENTER); + output.setSpacing(10.0); + + Button defaults = buttonBuilder("Save Defaults"); + defaults.setOnAction( (event) -> + { + GuiController.saveDefaults(); + GuiController.updateStart(); + }); + + Button save = buttonBuilder("Save"); + save.setOnAction( (event) -> + { + GuiController.save(); + GuiController.updateStart(); + }); + + Button saveClose = buttonBuilder("Save and Close"); + saveClose.setOnAction( (event) -> + { + GuiController.saveClose(); + GuiController.updateStart(); + STAGE.setScene(MAIN_MENU); + }); + + Button close = buttonBuilder("Close without Saving"); + close.setOnAction( (event) -> + { + STAGE.setScene(MAIN_MENU); + }); + + output.getChildren().addAll(defaults, + save, + saveClose, + close); + return output; + } + + /** + * Modifying function for a text field. + * + * Brings in an HBox, stores it in the Map with the correct {@link ConfigProperties} value. + * Also sets the action of the text box to be correct. + * + * @param hbox The HBox containing the TextField to be modified and remembered + * @param property The property to be associated with the TextField + * @param cameraName The name of the camera to be associated with the TextField + */ + private static void textFieldSetup(HBox hbox, ConfigProperties property, String cameraName) + { + TextField field = null; + for(Node child : hbox.getChildren()) + { + if(child instanceof TextField) + { + field = (TextField)child; + break; + } + } + if(field == null) + { + ErrorLogging.logError("GUI INIT ERROR!!! - Failed text field setup."); + GuiController.closeModel(); + } + + //GuiController.addToMap(cameraName,property,field); + Map cameraFields = uiFields.get(cameraName); + if(cameraFields.containsKey(property)) + { ErrorLogging.logError("GUI Setup Error!!! - Duplicate field: " + cameraName + " " + property.getConfig()); } + cameraFields.put(property,field); + uiFields.replace(cameraName,cameraFields); + field.setId(property.getConfig() + cameraName); + field.textProperty().addListener( + (observable, oldValue, newValue) -> + { + try(Scanner sc = new Scanner(newValue);) + { GuiController.setConfigValue(cameraName,property,sc.nextInt()); } + catch(Exception e) + { + ErrorLogging.logError("USER INPUT ERROR: Illegal input in " + property.getConfig() + " for " + cameraName + "."); + newValue = oldValue; + } + }); + } + + /** + * Builder function for a button. + * + * Creates a button with a set ID, name, and disabled/enables status. + * + * @param name The name of the new button + * @param disabled Whether or not the button should be disabled on startup + * + * @return Button , with a preset ID, name, and optionally disabled. + */ + private static Button buttonBuilder(String name,boolean disabled) + { + String[] id = name.strip().substring(0, name.length() - 1).toLowerCase().strip().split(" "); + Button button = new Button(name); + button.setId(id[0]); + button.setDisable(disabled); + return button; + } + + /** + * Builder function for an enabled button. + * + * Creates a button with a set ID and name. + * + * @param name The name of the new button + * @return Button , with a preset ID and name. + */ + private static Button buttonBuilder(String name) + { return buttonBuilder(name,false); } + + /** + * Builder function for a user-interactable TextField, with built-in label. + * + * Creates an HBox, with a Label for the TextField, along with the TextField itself. + * + * @param prompt The name used for the Label + * @param baseValue The default value used in the TextField + * @param description The Tooltip of the TextField/Label + * + * @return Hbox, described above + */ + private static HBox userTextField(String prompt, String baseValue, String description) + { + HBox output = new HBox(); + output.setSpacing(INTERNAL_SPACING); + output.setAlignment(Pos.CENTER_LEFT); + Label label = new Label(prompt); + TextField field = new TextField(); + String[] id = prompt.strip().substring(0, prompt.length() - 1).toLowerCase().strip().split(" "); + field.setId(id[0]); + output.setId(id[0] + "-box"); + field.setPromptText(baseValue); + Tooltip tooltip = new Tooltip(description); + field.setTooltip(tooltip); + label.setTooltip(tooltip); + output.getChildren().addAll(label,field); + return output; + } + + /** + * Getter for a given TextField, associated with a camera and property. + * + * @param cameraName The name of the camera the TextField is associated with + * @param property The name of the property the TextField is associated with + * + * @return TextField + */ + public static TextField getField(String cameraName, ConfigProperties property) + { return uiFields.get(cameraName).get(property); } + + /** + * Getter for the Start button. + * + * @return Button used for starting the tests. + */ + public static Button getStart() + { return startButton; } + + /** + * Getter for the user feedback Text object. + * + * @return Text object used for communicating statuses to the user. + */ + public static Text getFeedbackText() + { return userFeedback; } + + /** + * Getter for the TextField used by the user to set the number of iterations. + * + * @return TextField + */ + public static TextField getIterationField() + { return iterationField; } + + /** + * Getter for the ImageView Map. + * + * @return Map with keys of the names of cameras, and the value of the corresponding imageview + */ + public static Map getViewMap() + { return viewMap; } + + /** + * Updater for a given camera's ImageView width + * + * @param cameraName Name of the camera being updated + */ + public static void updateImageViewWidth(String cameraName) + { + viewMap.get(cameraName).setFitWidth(GuiController.getConfigValue(cameraName,ConfigProperties.CROP_W)); + } + + /** + * Updater for a given camera's ImageView height + * + * @param cameraName Name of the camera being updated + */ + public static void updateImageViewHeight(String cameraName) + { + viewMap.get(cameraName).setFitHeight(GuiController.getConfigValue(cameraName,ConfigProperties.CROP_H)); + } +}