* This class declares the main method used when running the GUI.
* In addition, all main GUI panes are declared and instantiated here.
*
* 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.
*
* 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.
*
* This service also handles flushing the console in a thread safe way. This
* is done by synchronizing the {@code nextGeneration()} and {@code flush()}
* method calls on a lock object.
*
* @author Eduardo Pedroni
*
*/
public class GUI extends Application {
/* Colours */
/**
* A string containing the hexadecimal colour used for representing neutrality.
*/
public static final String NEUTRAL_COLOUR = "#FFFFFF";
/**
* A string containing the hexadecimal colour used for representing a hard highlight.
* A "hard" select, for instance, happens when an output path is locked on the chromosome
* pane.
*/
public static final String HARD_HIGHLIGHT_COLOUR = "#5496FF";
/**
* A string containing the hexadecimal colour used for a medium highlight.
* One example of such a selection is the colour applied to a node
* when it is hovered over.
*/
public static final String MEDIUM_HIGHLIGHT_COLOUR = "#75BAFF";
/**
* A string containing the hexadecimal colour used for a soft highlight.
* When hovering over a node, its connections are soft-selected.
*/
public static final String SOFT_HIGHLIGHT_COLOUR = "#C7DFFF";
/**
* A string containing the hexadecimal colour used for representing a good selection.
* Ideally a shade of green, used for instance when a manual connection is valid.
*/
public static final String GOOD_SELECTION_COLOUR = "#38C25B";
/**
* A string containing the hexadecimal colour used for representing a neutral selection.
* Ideally a shade of yellow, used for instance when a manual connection is already the
* current connection.
*/
public static final String NEUTRAL_SELECTION_COLOUR = "#FFEF73";
/**
* A string containing the hexadecimal colour used for representing a bad selection.
* Ideally a shade of red, use for instance when a manual connection is not valid.
*/
public static final String BAD_SELECTION_COLOUR = "#FF5C5C";
/* Sizes and distances */
/**
* The width or height of the area that can be clicked on
* to drag-resize a pane.
*/
public static final double RESIZE_MARGIN = 5.0;
/**
* The minimum width of the settings pane, to prevent it
* from being resized beyond visibility.
*/
public static final double SETTINGS_MIN_WIDTH = 200;
/**
* The minimum width of the console pane, to prevent it
* from being resized beyond visibility.
*/
public static final double CONSOLE_MIN_HEIGHT = 100;
/*
* 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 jcgpService;
private Runnable consoleFlush;
/*
* The experiment itself
*/
private final JCGP jcgp;
/**
* 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();
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 () {
@Override
protected Task createTask() {
Task t = new Task() {
@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() {
@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.
*
* 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()}.
*
* 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();
}
populationPane.unlockOutputs();
jcgp.nextGeneration();
console.flush();
populationPane.updateGenes();
populationPane.relockOutputs();
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.
*
* 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) {
populationPane.unlockOutputs();
if (settingsPane.isResetRequired()) {
reset();
}
} else {
populationPane.updateGenes();
populationPane.relockOutputs();
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