From 36926be30bea635796e4b690b6db4a9531635329 Mon Sep 17 00:00:00 2001 From: Blizzard Finnegan Date: Wed, 8 Feb 2023 08:57:13 -0500 Subject: [PATCH 1/2] Add current state of Gui files --- pom.xml | 2 +- src/main/java/module-info.java | 6 +- .../org/baxter/disco/ocr/GuiController.java | 261 +++++ .../java/org/baxter/disco/ocr/GuiModel.java | 312 ++++++ .../java/org/baxter/disco/ocr/GuiStarter.java | 16 + .../java/org/baxter/disco/ocr/GuiView.java | 916 ++++++++++++++++++ 6 files changed, 1509 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/baxter/disco/ocr/GuiController.java create mode 100644 src/main/java/org/baxter/disco/ocr/GuiModel.java create mode 100644 src/main/java/org/baxter/disco/ocr/GuiStarter.java create mode 100644 src/main/java/org/baxter/disco/ocr/GuiView.java diff --git a/pom.xml b/pom.xml index 77289ad..a0c9e27 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 856007c..80e8d13 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,15 +3,15 @@ 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.fxml; + requires javafx.controls; requires org.apache.poi.poi; requires org.apache.commons.configuration2; requires org.apache.xmlbeans; requires org.bytedeco.tesseract; 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)); + } +} From e064440944c49e3a8e172fb072d9d09da43fe6a1 Mon Sep 17 00:00:00 2001 From: Blizzard Finnegan Date: Tue, 21 Mar 2023 09:58:06 -0400 Subject: [PATCH 2/2] Add back documentation notes to GUI branch --- MVC Diagram 2.gif | Bin 0 -> 14633 bytes MVC diagram.gif | Bin 0 -> 29995 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 MVC Diagram 2.gif create mode 100644 MVC diagram.gif diff --git a/MVC Diagram 2.gif b/MVC Diagram 2.gif new file mode 100644 index 0000000000000000000000000000000000000000..b5543fdd3331e32cd9780ea1894ff86ed62a1ce4 GIT binary patch literal 14633 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 literal 0 HcmV?d00001 diff --git a/MVC diagram.gif b/MVC diagram.gif new file mode 100644 index 0000000000000000000000000000000000000000..300f5a25c02e2a423e7e778a18e7f5cb23dbdd62 GIT binary patch literal 29995 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 X7Wb68=|zd~vwj0M>U-PFOn}P&K!O1+ literal 0 HcmV?d00001