aboutsummaryrefslogtreecommitdiffstats
path: root/src/jcgp/gui/GUI.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/jcgp/gui/GUI.java')
-rw-r--r--src/jcgp/gui/GUI.java477
1 files changed, 477 insertions, 0 deletions
diff --git a/src/jcgp/gui/GUI.java b/src/jcgp/gui/GUI.java
new file mode 100644
index 0000000..79678bc
--- /dev/null
+++ b/src/jcgp/gui/GUI.java
@@ -0,0 +1,477 @@
+package jcgp.gui;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.concurrent.Service;
+import javafx.concurrent.Task;
+import javafx.event.EventHandler;
+import javafx.scene.Scene;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Pane;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import jcgp.JCGP;
+import jcgp.backend.modules.problem.TestCaseProblem.TestCase;
+import jcgp.backend.resources.Resources;
+import jcgp.gui.console.ConsolePane;
+import jcgp.gui.dragresize.HorizontalDragResize;
+import jcgp.gui.dragresize.VerticalDragResize;
+import jcgp.gui.population.FunctionSelector;
+import jcgp.gui.population.GUINode;
+import jcgp.gui.population.PopulationPane;
+import jcgp.gui.settings.SettingsPane;
+
+/**
+ * Main class for the graphical user interface (GUI).
+ * <br><br>
+ * This class declares the main method used when running the GUI.
+ * In addition, all main GUI panes are declared and instantiated here.
+ * <br><br>
+ * The user interface is divided into 3 main components: the node grid
+ * ({@link PopulationPane}), the control pane ({@link SettingsPane}) and
+ * the console ({@link ConsolePane}). Click on any of the links in
+ * brackets to see more information about each interface component.
+ * <br><br>
+ * This class also contains the instance of JCGP responsible for
+ * running the experiments in GUI mode. JCGP's execution must be delegated
+ * to a separate thread so that the GUI remains unblocked. This is done using
+ * a JavaFX {@code Service} which calls {@code nextGeneration()} in a loop
+ * until it is interrupted by the main JavaFX thread.
+ * <br>
+ * This service also handles flushing the console in a thread safe way. This
+ * is done by synchronising the {@code nextGeneration()} and {@code flush()}
+ * method calls on a lock object.
+ *
+ * @author Eduardo Pedroni
+ *
+ */
+public class GUI extends Application {
+
+ /*
+ * Actual GUI elements
+ */
+ private Stage stage;
+ private PopulationPane populationPane;
+ private ConsolePane console;
+ private SettingsPane settingsPane;
+ private final FunctionSelector functionSelector;
+
+ /*
+ * Flow control objects
+ */
+ private boolean running = false;
+ private final Object printLock = new Object();
+ private Service<Void> jcgpService;
+ private Runnable consoleFlush;
+
+ /*
+ * The experiment itself
+ */
+ private final JCGP jcgp;
+
+ public static Resources resources;
+
+
+ /**
+ * Start JCGP with the user interface.
+ *
+ * @param args no arguments are used.
+ */
+ public static void main(String[] args) {
+ // not much to do, simply launch the JavaFX application
+ launch();
+ }
+
+ /**
+ * Makes a new instance of GUI. This initialises the JCGP experiment and
+ * instantiates the function selector. It also creates the console flush task
+ * and the service responsible for running the JCGP experiment.
+ */
+ public GUI() {
+ jcgp = new JCGP();
+ resources = jcgp.getResources();
+ functionSelector = new FunctionSelector(jcgp.getResources().getFunctionSet());
+
+ /*
+ * This task flushes the console in a thread-safe way.
+ * The problem is that this task is executed using Platform.runLater()
+ * to ensure that the flush itself happens on the JavaFX thread. However,
+ * runLater() is not guaranteed to run anytime soon. If the time taken for
+ * jcgp to perform a single generation is shorter than the time taken for
+ * this task to be executed by the platform, consoleFlush tasks will be
+ * scheduled faster than they can be executed and the console will eventually
+ * freeze.
+ *
+ * This is addressed by synchronizing the flushes with each nextGeneration() call.
+ */
+ consoleFlush = new Runnable() {
+ @Override
+ public void run() {
+ /*
+ * Try to acquire printlock - wait here until jcgpService relinquishes it
+ * by calling wait(). This means that it is finished with the current generation
+ * and will wait for the console to be flushed to move on.
+ * It might be the case that the service has already released the lock by waiting
+ * on it; it makes no difference. In that case this will acquire the lock
+ * immediately and proceed to flush the console.
+ */
+ synchronized(printLock) {
+ /*
+ * The lock is acquired, at this point we are certain that jcgpService
+ * cannot execute; it is currently waiting to be notified about the lock.
+ * No additional consoleFlush tasks can be scheduled with runLater() because
+ * the service is waiting. We can now take our time to flush the console.
+ */
+ console.flush();
+ /*
+ * Once the console finishes flushing, we notify jcgpService to perform the
+ * next generation.
+ */
+ printLock.notifyAll();
+ }
+ }
+ };
+
+ /*
+ * This service runs on a separate thread and performs
+ * the experiment, including console prints, in a thread-safe
+ * way. It is synchronized with consoleFlush.
+ */
+ jcgpService = new Service<Void> () {
+ @Override
+ protected Task<Void> createTask() {
+ Task<Void> t = new Task<Void>() {
+ @Override
+ protected Void call() throws Exception {
+ /*
+ * Only execute if the experiment isn't finished
+ * and the service hasn't been cancelled.
+ */
+ while (!isCancelled() && !jcgp.isFinished()) {
+ /*
+ * Attempt to acquire the printlock.
+ * Successfully doing so means no printing
+ * is currently taking place and we are free
+ * to schedule a print task.
+ * This lock acquisition should never block. It should
+ * not be possible to execute this statement without
+ * having been notified by consoleFlush.
+ */
+ synchronized (printLock) {
+ /*
+ * Lock has been acquired, schedule a print
+ * task ahead of time. The actual print messages
+ * haven't been send to the console yet, that happens
+ * during nextGeneration(), but since we have the lock
+ * consoleFlush() will sit and wait for us to release it
+ * whenever we are finished queueing prints.
+ */
+ Platform.runLater(consoleFlush);
+ /*
+ * Perform the actual generation. Here zero or more
+ * strings might be sent to the console buffer.
+ */
+ jcgp.nextGeneration();
+ /*
+ * The generation is complete, relinquish the lock.
+ * By this point chances are the platform is already trying
+ * to execute the consoleFlush task that we scheduled. If it
+ * hasn't already started though, it doesn't matter; we will
+ * wait for a notification on the lock, which will only come
+ * when printing is complete.
+ */
+ printLock.wait();
+ /*
+ * We have been notified. This means all buffered messages have
+ * been successfully flushed to the actual console control and
+ * we are now ready to perform another generation (or break out
+ * of the loop if the loop conditions are no longer met).
+ */
+ }
+ /*
+ * We no longer own the lock, but neither does consoleFlush.
+ * The synchrony cycle has returned to its initial state, and we
+ * are free to acquire the lock again.
+ */
+ }
+ /*
+ * Something happened to break the while loop -
+ * either the experiment finished or the user pressed
+ * pause.
+ */
+ if (jcgp.isFinished()) {
+ // the experiment has finished, switch to pause mode
+ Platform.runLater(new Runnable() {
+ @Override
+ public void run() {
+ runningMode(false);
+ }
+ });
+ }
+ return null;
+ }
+ };
+ return t;
+ }
+ };
+ }
+
+ @Override
+ public void start(Stage primaryStage) throws Exception {
+ /*
+ * This method gets called when the application launches. Once it
+ * returns, the application falls into the main loop which handles
+ * events, so all elements must be constructed here.
+ */
+
+ // make the console and set it so it is used for JCGP prints
+ console = new ConsolePane();
+ jcgp.setConsole(console);
+
+ // store reference to the stage
+ stage = primaryStage;
+
+ /*
+ * The experiment layer contains all of the experiment-related panes.
+ * The only element that sits higher than this is the function selector.
+ */
+ BorderPane experimentLayer = new BorderPane();
+ /*
+ * The left frame encapsulates the population pane and the console.
+ * It goes into the center position of the experiment layer, next to the settings pane.
+ */
+ BorderPane leftFrame = new BorderPane();
+
+ /*
+ * The population pane is a TabPane containing a tab for each chromosome.
+ */
+ populationPane = new PopulationPane(this);
+
+ /*
+ * The settings pane is a big class containing the entire control pane
+ */
+ settingsPane = new SettingsPane(this);
+
+ // make control pane and console resizable
+ HorizontalDragResize.makeDragResizable(settingsPane);
+ VerticalDragResize.makeDragResizable(console);
+ // prevent resizables from growing larger than the experiment layer
+ settingsPane.maxWidthProperty().bind(experimentLayer.widthProperty());
+ console.maxHeightProperty().bind(experimentLayer.heightProperty());
+
+ // put console and population pane in the main frame
+ leftFrame.setCenter(populationPane);
+ leftFrame.setBottom(console);
+
+ // set the main frame and the control pane in the experiment layer
+ experimentLayer.setCenter(leftFrame);
+ experimentLayer.setRight(settingsPane);
+
+ /*
+ * Now we deal with the stage.
+ */
+ primaryStage.setTitle("JCGP");
+
+ // this pane holds the entire scene, that is its sole job.
+ Pane sceneParent = new Pane();
+ // the experiment layer should fill the entire scene parent
+ experimentLayer.prefHeightProperty().bind(sceneParent.heightProperty());
+ experimentLayer.prefWidthProperty().bind(sceneParent.widthProperty());
+ // the function selector goes over the experiment layer so it doesn't get covered by other panes
+ sceneParent.getChildren().addAll(experimentLayer, functionSelector);
+
+ // set the scene, minimum sizes, show
+ primaryStage.setScene(new Scene(sceneParent));
+ primaryStage.setMinWidth(800);
+ primaryStage.setMinHeight(600);
+ primaryStage.show();
+
+ // when the main stage closes, close the test case table as well
+ primaryStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
+ @Override
+ public void handle(WindowEvent event) {
+ if (settingsPane.getTestCaseTable() != null) {
+ settingsPane.getTestCaseTable().close();
+ }
+ }
+ });
+ }
+
+ /**
+ * Run/pause method.
+ * Run the experiment if it is paused, or pause it if it is running.
+ * <br>
+ * This method is the callback used by the run/pause button. It
+ * controls the jcgp service.
+ */
+ public void runPause() {
+ // do nothing if experiment is finished or parameters aren't valid
+ if (!jcgp.isFinished() && settingsPane.areParametersValid()) {
+ if (!running) {
+ runningMode(true);
+ jcgpService.restart();
+ } else {
+ jcgpService.cancel();
+ runningMode(false);
+ }
+ }
+ }
+
+ /**
+ * Perform a single generation using {@code nextGeneration()}.
+ * <br>
+ * On top of that, this method performs all of the housekeeping
+ * that is normally done before and after running, such as
+ * refreshing the chromosome panes.
+ */
+ public void step() {
+ // do nothing if experiment is finished, running or parameters aren't valid
+ if (!running && !jcgp.isFinished() && settingsPane.areParametersValid()) {
+ if (settingsPane.isResetRequired()) {
+ reset();
+ }
+ jcgp.nextGeneration();
+ console.flush();
+
+ populationPane.updateGenes();
+ settingsPane.revalidateParameters();
+ settingsPane.updateControls(false, jcgp.isFinished());
+ }
+ }
+
+ /**
+ * Reset button callback. If the parameters are valid,
+ * this resets the entire experiment by calling {@code reset()}
+ * on jcgp.
+ */
+ public void reset() {
+ if (!running && settingsPane.areParametersValid()) {
+ setEvaluating(false);
+ jcgp.reset();
+ settingsPane.applyParameters();
+ reDraw();
+ }
+ }
+
+ /**
+ * Does a complete GUI refresh.
+ * This is potentially lengthy, so use with care.
+ */
+ public void reDraw() {
+ populationPane.remakeTabs();
+ settingsPane.revalidateParameters();
+ settingsPane.updateControls(false, jcgp.isFinished());
+ console.flush();
+ }
+
+ /**
+ * Toggles the entire GUI between run and pause
+ * mode.
+ * <br><br>
+ * A lot of the GUI must be enabled or disabled
+ * depending on what the experiment is doing. This
+ * method provides a one-line way to make
+ * all required adjustments.
+ *
+ * @param value true if experiment is running, false otherwise.
+ */
+ private void runningMode(boolean value) {
+ if (value) {
+ if (settingsPane.isResetRequired()) {
+ reset();
+ }
+ } else {
+ populationPane.updateGenes();
+ settingsPane.revalidateParameters();
+ }
+ populationPane.setDisable(value);
+ settingsPane.updateControls(value, jcgp.isFinished());
+
+ running = value;
+ }
+
+ /**
+ * Refresh the function selector, used when functions are enabled or disabled.
+ */
+ public void updateFunctionSelector() {
+ functionSelector.remakeFunctions(jcgp.getResources().getFunctionSet());
+ }
+
+ /**
+ * @return true if jcgp is evolving.
+ */
+ public boolean isWorking() {
+ return running;
+ }
+
+ /**
+ * Relocate the function selector to the right position
+ * relative to the specified node and set it visible.
+ *
+ * @param event the mouse event containing cursor coordinates.
+ * @param node the node whose function should be changed.
+ */
+ public void bringFunctionSelector(MouseEvent event, GUINode node) {
+ functionSelector.relocateAndShow(event, node);
+ }
+
+ /**
+ * @return a reference to the {@code JCGP} experiment.
+ */
+ public JCGP getExperiment() {
+ return jcgp;
+ }
+
+ /**
+ * Starts the evaluation process with the given test case.
+ * It does so by calling {@code evaluateTestCase()} on
+ * the population pane.
+ *
+ * @param testCase the test case to evaluate.
+ */
+ public void evaluateTestCase(TestCase<Object> testCase) {
+ populationPane.evaluateTestCase(testCase);
+ }
+
+ /**
+ * Hide all evaluated values. This should be called when
+ * evaluations are no longer being performed.
+ */
+ public void hideGeneValues() {
+ populationPane.hideValues();
+ }
+
+ /**
+ * Set the system into evaluation mode.
+ * When in evaluation mode, the population pane
+ * refreshes the node values whenever connection
+ * changes happen.
+ *
+ * @param value true if evaluations are happening, false otherwise.
+ */
+ public void setEvaluating(boolean value) {
+ populationPane.setEvaluating(value);
+ }
+
+ /**
+ * @return a reference to the GUI stage.
+ */
+ public Stage getStage() {
+ return stage;
+ }
+
+ /**
+ * Writes all buffered content out to the GUI console.
+ */
+ public void flushConsole() {
+ console.flush();
+ }
+
+ /**
+ * @return the index of the chromosome currently being looked at.
+ */
+ public int getChromosomeIndex() {
+ return populationPane.getSelectionModel().getSelectedIndex();
+ }
+}