<commit_before>package programminglife.gui.controller;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import javafx.stage.Stage;
import jp.uphy.javafx.console.ConsoleView;
import programminglife.ProgrammingLife;
import programminglife.model.GenomeGraph;
import programminglife.model.exception.UnknownTypeException;
import programminglife.parser.GraphParser;
import programminglife.utility.Alerts;
import programminglife.utility.Console;
import programminglife.utility.FileProgressCounter;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.Scanner;

/**
 * The controller for the GUI that is used in the application.
 * The @FXML tag is needed in initialize so that javaFX knows what to do.
 */
public class GuiController implements Observer {
    //static finals
    private static final String INITIAL_CENTER_NODE = "1";
    private static final String INITIAL_MAX_DRAW_DEPTH = "10";

    //FXML imports.
    @FXML private MenuItem btnOpen;
    @FXML private MenuItem btnQuit;
    @FXML private MenuItem btnBookmarks;
    @FXML private MenuItem btnAbout;
    @FXML private MenuItem btnInstructions;
    @FXML private Menu menuRecent;
    @FXML private RadioMenuItem btnToggle;
    @FXML private Button btnZoomReset;
    @FXML private Button btnTranslateReset;
    @FXML private Button btnDraw;
    @FXML private Button btnDrawRandom;
    @FXML private Button btnBookmark;
    @FXML private ProgressBar progressBar;

    @FXML private TextField txtMaxDrawDepth;
    @FXML private TextField txtCenterNode;

    @FXML private Group grpDrawArea;
    @FXML private AnchorPane anchorLeftControlPanel;
    @FXML private AnchorPane anchorGraphPanel;

    private double orgSceneX, orgSceneY;
    private double orgTranslateX, orgTranslateY;
    private GraphController graphController;
    private File file;
    private File recentFile = new File("Recent.txt");
    private String recentItems = "";
    private Thread parseThread;
    private static final double MAX_SCALE = 10.0d;
    private static final double MIN_SCALE = .1d;
    private static final double ZOOM_FACTOR = 1.05d;
    private double boundsWidth;
    private double boundsHeight;

    /**
     * The initialize will call the other methods that are run in the .
     */
    @FXML
    @SuppressWarnings("unused")
    private void initialize() {
        this.graphController = new GraphController(null, this.grpDrawArea);
        initRecent();
        initMenubar();
        initBookmarkMenu();
        initLeftControlpanelScreenModifiers();
        initLeftControlpanelDraw();
        initMouse();
        initConsole();
    }

    /**
     * Open and parse a file.
     * @param file The {@link File} to open.
     * @throws IOException if the {@link File} is not found.
     * @throws UnknownTypeException if the {@link File} is not compliant with the GFA standard.
     * @return the parser to be notified when it is finished
     */
    public GraphParser openFile(File file) throws IOException, UnknownTypeException {
        if (file != null) {
            if (this.graphController != null && this.graphController.getGraph() != null) {
                this.graphController.getGraph().close();
                this.grpDrawArea.getChildren().clear();
            }

            disableGraphUIElements(true);

            GraphParser graphParser = new GraphParser(file);
            graphParser.addObserver(this);
            graphParser.getProgressCounter().addObserver(this);

            if (this.parseThread != null) {
                this.parseThread.interrupt();
            }
            this.parseThread = new Thread(graphParser);
            this.parseThread.start();

            return graphParser;
        }

        return null;
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof GraphParser) {
            if (arg instanceof GenomeGraph) {
                GenomeGraph graph = (GenomeGraph) arg;

                Console.println("[%s] File Parsed.", Thread.currentThread().getName());

                this.setGraph(graph);
            } else if (arg instanceof Exception) {
                Exception e = (Exception) arg;
                e.printStackTrace();
                Alerts.error(e.getMessage());
            }
        } else if (o instanceof FileProgressCounter) {
            progressBar.setVisible(true);
            FileProgressCounter progress = (FileProgressCounter) o;
            this.getProgressBar().setProgress(progress.percentage());
            if (progressBar.getProgress() == 1.0d) {
                progressBar.setVisible(false);
            }
        }
    }

    /**
     * Set the graph for this GUIController.
     * @param graph Graph to use.
     */
    public void setGraph(GenomeGraph graph) {
        this.graphController.setGraph(graph);
        disableGraphUIElements(graph == null);
        Platform.runLater(() -> {
            assert graph != null;
            ProgrammingLife.getStage().setTitle(graph.getID());
        });

        if (graph != null) {
            Console.println("[%s] Graph was set to %s.", Thread.currentThread().getName(), graph.getID());
            Console.println("[%s] The graph has %d nodes", Thread.currentThread().getName(), graph.size());
        }
    }

    /**
     * Read out the file which contains all the recently opened files.
     */
    private void initRecent() {
        try {
            Files.createFile(recentFile.toPath());
        } catch (FileAlreadyExistsException e) {
            //This will always happen if a user has used the program before.
            //Therefore it is unnecessary to handle further.
        } catch (IOException e) {
            Alerts.error("This file can't be opened");
            return;
        }
        if (recentFile != null) {
            try (Scanner sc = new Scanner(recentFile)) {
                menuRecent.getItems().clear();
                while (sc.hasNextLine()) {
                    String next = sc.nextLine();
                    MenuItem mi = new MenuItem(next);
                    mi.setOnAction(event -> {
                        try {
                            file = new File(mi.getText());
                            openFile(file);
                        } catch (UnknownTypeException e) {
                            Alerts.error("This file is malformed and cannot be parsed");
                        } catch (IOException e) {
                            Alerts.error("This file can't be opened");
                        }
                    });
                    menuRecent.getItems().add(mi);
                    recentItems = recentItems.concat(next + System.getProperty("line.separator"));
                }
            } catch (FileNotFoundException e) {
                Alerts.error("This file can't be found");
            }
        }
    }

    /**
     * Initializes the open button so that the user can decide which file to open.
     * Sets the action for the open MenuItem.
     * Sets the event for the quit MenuItem.
     */
    private void initMenubar() {
        btnOpen.setOnAction((ActionEvent event) -> {
            FileChooser fileChooser = new FileChooser();
            final ExtensionFilter extFilterGFA = new ExtensionFilter("GFA files (*.gfa)", "*.GFA");
            fileChooser.getExtensionFilters().add(extFilterGFA);
            if (file != null) {
                File existDirectory = file.getParentFile();
                fileChooser.setInitialDirectory(existDirectory);
            }
            try {
                file = fileChooser.showOpenDialog(ProgrammingLife.getStage());
                if (file != null) {
                    this.openFile(file);
                    updateRecent();
                }
            } catch (FileNotFoundException e) {
                Alerts.error("This file can't be found");
            } catch (IOException e) {
                Alerts.error("This file can't be opened");
            } catch (UnknownTypeException e) {
                Alerts.error("This file is malformed and cannot be parsed");
            }
        });

        btnOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCodeCombination.CONTROL_DOWN));
        btnQuit.setOnAction(event -> Alerts.quitAlert());
        btnQuit.setAccelerator(new KeyCodeCombination(KeyCode.E, KeyCodeCombination.CONTROL_DOWN));
        btnAbout.setOnAction(event -> Alerts.infoAboutAlert());
        btnInstructions.setOnAction(event -> Alerts.infoInstructionAlert());
    }

    /**
     * Updates the recent files file after opening a file.
     */
    private void updateRecent() {
        try (BufferedWriter recentsWriter = new BufferedWriter(new FileWriter(recentFile, true))) {
            if (!recentItems.contains(file.getAbsolutePath())) {
                recentsWriter.write(file.getAbsolutePath() + System.getProperty("line.separator"));
                recentsWriter.flush();
                recentsWriter.close();
                initRecent();
            }
        } catch (IOException e) {
            Alerts.error("This file can't be updated");
        }
    }

    /**
     * Initializes the bookmark buttons in the menu.
     */
    private void initBookmarkMenu() {

        btnBookmarks.setOnAction(event -> {
            try {
                FXMLLoader loader = new FXMLLoader(ProgrammingLife.class.getResource("/LoadBookmarkWindow.fxml"));
                AnchorPane page = loader.load();
                GuiLoadBookmarkController gc = loader.getController();
                gc.setGuiController(this);
                gc.initBookmarks();
                if (this.graphController.getGraph() != null) {
                    gc.setBtnCreateBookmarkActive(true);
                }
                Scene scene = new Scene(page);
                Stage bookmarkDialogStage = new Stage();
                bookmarkDialogStage.setResizable(false);
                bookmarkDialogStage.setScene(scene);
                bookmarkDialogStage.setTitle("Load Bookmark");
                bookmarkDialogStage.initOwner(ProgrammingLife.getStage());
                bookmarkDialogStage.showAndWait();
            } catch (IOException e) {
                Alerts.error("The bookmarks file can't be opened");
            }
        });
        btnBookmarks.setAccelerator(new KeyCodeCombination(KeyCode.B, KeyCodeCombination.CONTROL_DOWN));
    }

    /**
     * Method to disable the UI Elements on the left of the GUI.
     * @param isDisabled boolean, true disables the left anchor panel.
     */
    private void disableGraphUIElements(boolean isDisabled) {
        anchorLeftControlPanel.setDisable(isDisabled);
    }

    /**
     * Initializes the buttons on the panel on the left side which do translation/zoom.
     */
    private void initLeftControlpanelScreenModifiers() {
        disableGraphUIElements(true);

        btnTranslateReset.setOnAction(event -> {
            grpDrawArea.setTranslateX(graphController.getLocationCenterX());
            grpDrawArea.setTranslateY(graphController.getLocationCenterY());
        });

        btnZoomReset.setOnAction(event -> {
            grpDrawArea.setScaleX(1);
            grpDrawArea.setScaleY(1);
        });
    }

    /**
     * Initializes the button on the left side that are used to draw the graph.
     */
    private void initLeftControlpanelDraw() {
        disableGraphUIElements(true);

        btnDraw.setOnAction(e -> this.draw());

        btnDrawRandom.setOnAction(event -> {
            int randomNodeID = (new Random()).nextInt(this.graphController.getGraph().size());
            txtCenterNode.setText(Integer.toString(randomNodeID));
            this.draw();
        });

        btnBookmark.setOnAction(event -> buttonBookmark());

        txtMaxDrawDepth.textProperty().addListener(new NumbersOnlyListener(txtMaxDrawDepth));
        txtMaxDrawDepth.setText(INITIAL_MAX_DRAW_DEPTH);

        txtCenterNode.textProperty().addListener(new NumbersOnlyListener(txtCenterNode));
        txtCenterNode.setText(INITIAL_CENTER_NODE);
    }

    /**
     * Draw the current graph with current center node and depth settings.
     */
    public void draw() {
        Console.println("[%s] Drawing graph...", Thread.currentThread().getName());
        int centerNode = 0;
        int maxDepth = 0;
        try {
            centerNode = Integer.parseInt(txtCenterNode.getText());
            try {
                maxDepth = Integer.parseInt(txtMaxDrawDepth.getText());
            } catch (NumberFormatException e) {
                Alerts.warning("Radius is not a number, try again with a number as input.");
            }
        } catch (NumberFormatException e) {
            Alerts.warning("Center node ID is not a number, try again with a number as input.");
        }

        if (graphController.getGraph().contains(centerNode)) {
            this.graphController.clear();
            this.graphController.draw(centerNode, maxDepth);
            Console.println("[%s] Graph drawn.", Thread.currentThread().getName());
        } else {
            Alerts.warning("The centernode is not a existing node, "
                    + "try again with a number that exists as a node.");
        }
    }

    /**
     * {@link ChangeListener} to make a {@link TextField} only accept numbers.
     */
    private class NumbersOnlyListener implements ChangeListener<String> {
        private final TextField tf;

        /**
         * Constructor for the Listener.
         * @param tf {@link TextField} is the text field on which the listener listens
         */
        NumbersOnlyListener(TextField tf) {
            this.tf = tf;
        }

        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            if (!newValue.matches("\\d")) {
                tf.setText(newValue.replaceAll("[^\\d]", ""));
            }
        }
    }

    /**
     * Handles the events of the bookmark button.
     */
    private void buttonBookmark() {
        try {
            FXMLLoader loader = new FXMLLoader(ProgrammingLife.class.getResource("/CreateBookmarkWindow.fxml"));
            AnchorPane page = loader.load();
            GuiCreateBookmarkController gc = loader.getController();
            gc.setGuiController(this);
            gc.setText(txtCenterNode.getText(), txtMaxDrawDepth.getText());
            Scene scene = new Scene(page);
            Stage bookmarkDialogStage = new Stage();
            bookmarkDialogStage.setResizable(false);
            bookmarkDialogStage.setScene(scene);
            bookmarkDialogStage.setTitle("Create Bookmark");
            bookmarkDialogStage.initOwner(ProgrammingLife.getStage());
            bookmarkDialogStage.showAndWait();
        } catch (IOException e) {
            Alerts.error("This bookmark cannot be created.");
        }
    }

    /**
     * Initialises the mouse events.
     */
    private void initMouse() {
        anchorGraphPanel.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
            orgSceneX = event.getSceneX();
            orgSceneY = event.getSceneY();
            orgTranslateX = grpDrawArea.getTranslateX();
            orgTranslateY = grpDrawArea.getTranslateY();
            event.consume();
        });
        anchorGraphPanel.addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
            grpDrawArea.setTranslateX(orgTranslateX + event.getSceneX() - orgSceneX);
            grpDrawArea.setTranslateY(orgTranslateY + event.getSceneY() - orgSceneY);
            event.consume();
        });
        anchorGraphPanel.addEventHandler(ScrollEvent.SCROLL, event -> {
            zoom(event.getDeltaX(), event.getDeltaY(), ZOOM_FACTOR);
        });
    }

    /**
     * Handles the zooming in and out of the group.
     * @param deltaX double for the x scale.
     * @param deltaY double for the y scale.
     * @param delta double the factor by which is zoomed.
     */
    private void zoom(double deltaX, double deltaY, double delta) {
        double scaleX = grpDrawArea.getScaleX();
        double scaleY = grpDrawArea.getScaleY();

        if (deltaX < 0 || deltaY < 0) {
            scaleX /= delta;
            scaleY /= delta;
        } else {
            scaleX *= delta;
            scaleY *= delta;
        }
        scaleX = clamp(scaleX, MIN_SCALE, MAX_SCALE);
        scaleY = clamp(scaleY, MIN_SCALE, MAX_SCALE);

        grpDrawArea.setScaleX(scaleX);
        grpDrawArea.setScaleY(scaleY);
    }

    /**
     * Clamp function used for zooming in and out.
     * @param value double current scale.
     * @param min double min scale value.
     * @param max double max scale value.
     * @return double scale value.
     */
    private static double clamp(double value, double min, double max) {

        if (Double.compare(value, min) < 0) {
            return min;
        }<commit_msg>Update on zooming<commit_after>package programminglife.gui.controller;

import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.transform.Scale;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import javafx.stage.Stage;
import jp.uphy.javafx.console.ConsoleView;
import programminglife.ProgrammingLife;
import programminglife.model.GenomeGraph;
import programminglife.model.exception.UnknownTypeException;
import programminglife.parser.GraphParser;
import programminglife.utility.Alerts;
import programminglife.utility.Console;
import programminglife.utility.FileProgressCounter;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.Scanner;

/**
 * The controller for the GUI that is used in the application.
 * The @FXML tag is needed in initialize so that javaFX knows what to do.
 */
public class GuiController implements Observer {
    //static finals
    private static final String INITIAL_CENTER_NODE = "1";
    private static final String INITIAL_MAX_DRAW_DEPTH = "10";

    //FXML imports.
    @FXML private MenuItem btnOpen;
    @FXML private MenuItem btnQuit;
    @FXML private MenuItem btnBookmarks;
    @FXML private MenuItem btnAbout;
    @FXML private MenuItem btnInstructions;
    @FXML private Menu menuRecent;
    @FXML private RadioMenuItem btnToggle;
    @FXML private Button btnZoomReset;
    @FXML private Button btnTranslateReset;
    @FXML private Button btnDraw;
    @FXML private Button btnDrawRandom;
    @FXML private Button btnBookmark;
    @FXML private ProgressBar progressBar;

    @FXML private TextField txtMaxDrawDepth;
    @FXML private TextField txtCenterNode;

    @FXML private Group grpDrawArea;
    @FXML private AnchorPane anchorLeftControlPanel;
    @FXML private AnchorPane anchorGraphPanel;

    private double orgSceneX, orgSceneY;
    private double orgTranslateX, orgTranslateY;
    private GraphController graphController;
    private File file;
    private File recentFile = new File("Recent.txt");
    private String recentItems = "";
    private Thread parseThread;
    private static final double MAX_SCALE = 10.0d;
    private static final double MIN_SCALE = .1d;
    private static final double ZOOM_FACTOR = 1.05d;
    private double boundsWidth;
    private double boundsHeight;

    /**
     * The initialize will call the other methods that are run in the .
     */
    @FXML
    @SuppressWarnings("unused")
    private void initialize() {
        this.graphController = new GraphController(null, this.grpDrawArea);
        initRecent();
        initMenubar();
        initBookmarkMenu();
        initLeftControlpanelScreenModifiers();
        initLeftControlpanelDraw();
        initMouse();
        initConsole();
    }

    /**
     * Open and parse a file.
     * @param file The {@link File} to open.
     * @throws IOException if the {@link File} is not found.
     * @throws UnknownTypeException if the {@link File} is not compliant with the GFA standard.
     * @return the parser to be notified when it is finished
     */
    public GraphParser openFile(File file) throws IOException, UnknownTypeException {
        if (file != null) {
            if (this.graphController != null && this.graphController.getGraph() != null) {
                this.graphController.getGraph().close();
                this.grpDrawArea.getChildren().clear();
            }

            disableGraphUIElements(true);

            GraphParser graphParser = new GraphParser(file);
            graphParser.addObserver(this);
            graphParser.getProgressCounter().addObserver(this);

            if (this.parseThread != null) {
                this.parseThread.interrupt();
            }
            this.parseThread = new Thread(graphParser);
            this.parseThread.start();

            return graphParser;
        }

        return null;
    }

    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof GraphParser) {
            if (arg instanceof GenomeGraph) {
                GenomeGraph graph = (GenomeGraph) arg;

                Console.println("[%s] File Parsed.", Thread.currentThread().getName());

                this.setGraph(graph);
            } else if (arg instanceof Exception) {
                Exception e = (Exception) arg;
                e.printStackTrace();
                Alerts.error(e.getMessage());
            }
        } else if (o instanceof FileProgressCounter) {
            progressBar.setVisible(true);
            FileProgressCounter progress = (FileProgressCounter) o;
            this.getProgressBar().setProgress(progress.percentage());
            if (progressBar.getProgress() == 1.0d) {
                progressBar.setVisible(false);
            }
        }
    }

    /**
     * Set the graph for this GUIController.
     * @param graph Graph to use.
     */
    public void setGraph(GenomeGraph graph) {
        this.graphController.setGraph(graph);
        disableGraphUIElements(graph == null);
        Platform.runLater(() -> {
            assert graph != null;
            ProgrammingLife.getStage().setTitle(graph.getID());
        });

        if (graph != null) {
            Console.println("[%s] Graph was set to %s.", Thread.currentThread().getName(), graph.getID());
            Console.println("[%s] The graph has %d nodes", Thread.currentThread().getName(), graph.size());
        }
    }

    /**
     * Read out the file which contains all the recently opened files.
     */
    private void initRecent() {
        try {
            Files.createFile(recentFile.toPath());
        } catch (FileAlreadyExistsException e) {
            //This will always happen if a user has used the program before.
            //Therefore it is unnecessary to handle further.
        } catch (IOException e) {
            Alerts.error("This file can't be opened");
            return;
        }
        if (recentFile != null) {
            try (Scanner sc = new Scanner(recentFile)) {
                menuRecent.getItems().clear();
                while (sc.hasNextLine()) {
                    String next = sc.nextLine();
                    MenuItem mi = new MenuItem(next);
                    mi.setOnAction(event -> {
                        try {
                            file = new File(mi.getText());
                            openFile(file);
                        } catch (UnknownTypeException e) {
                            Alerts.error("This file is malformed and cannot be parsed");
                        } catch (IOException e) {
                            Alerts.error("This file can't be opened");
                        }
                    });
                    menuRecent.getItems().add(mi);
                    recentItems = recentItems.concat(next + System.getProperty("line.separator"));
                }
            } catch (FileNotFoundException e) {
                Alerts.error("This file can't be found");
            }
        }
    }

    /**
     * Initializes the open button so that the user can decide which file to open.
     * Sets the action for the open MenuItem.
     * Sets the event for the quit MenuItem.
     */
    private void initMenubar() {
        btnOpen.setOnAction((ActionEvent event) -> {
            FileChooser fileChooser = new FileChooser();
            final ExtensionFilter extFilterGFA = new ExtensionFilter("GFA files (*.gfa)", "*.GFA");
            fileChooser.getExtensionFilters().add(extFilterGFA);
            if (file != null) {
                File existDirectory = file.getParentFile();
                fileChooser.setInitialDirectory(existDirectory);
            }
            try {
                file = fileChooser.showOpenDialog(ProgrammingLife.getStage());
                if (file != null) {
                    this.openFile(file);
                    updateRecent();
                }
            } catch (FileNotFoundException e) {
                Alerts.error("This file can't be found");
            } catch (IOException e) {
                Alerts.error("This file can't be opened");
            } catch (UnknownTypeException e) {
                Alerts.error("This file is malformed and cannot be parsed");
            }
        });

        btnOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCodeCombination.CONTROL_DOWN));
        btnQuit.setOnAction(event -> Alerts.quitAlert());
        btnQuit.setAccelerator(new KeyCodeCombination(KeyCode.E, KeyCodeCombination.CONTROL_DOWN));
        btnAbout.setOnAction(event -> Alerts.infoAboutAlert());
        btnInstructions.setOnAction(event -> Alerts.infoInstructionAlert());
    }

    /**
     * Updates the recent files file after opening a file.
     */
    private void updateRecent() {
        try (BufferedWriter recentsWriter = new BufferedWriter(new FileWriter(recentFile, true))) {
            if (!recentItems.contains(file.getAbsolutePath())) {
                recentsWriter.write(file.getAbsolutePath() + System.getProperty("line.separator"));
                recentsWriter.flush();
                recentsWriter.close();
                initRecent();
            }
        } catch (IOException e) {
            Alerts.error("This file can't be updated");
        }
    }

    /**
     * Initializes the bookmark buttons in the menu.
     */
    private void initBookmarkMenu() {

        btnBookmarks.setOnAction(event -> {
            try {
                FXMLLoader loader = new FXMLLoader(ProgrammingLife.class.getResource("/LoadBookmarkWindow.fxml"));
                AnchorPane page = loader.load();
                GuiLoadBookmarkController gc = loader.getController();
                gc.setGuiController(this);
                gc.initBookmarks();
                if (this.graphController.getGraph() != null) {
                    gc.setBtnCreateBookmarkActive(true);
                }
                Scene scene = new Scene(page);
                Stage bookmarkDialogStage = new Stage();
                bookmarkDialogStage.setResizable(false);
                bookmarkDialogStage.setScene(scene);
                bookmarkDialogStage.setTitle("Load Bookmark");
                bookmarkDialogStage.initOwner(ProgrammingLife.getStage());
                bookmarkDialogStage.showAndWait();
            } catch (IOException e) {
                Alerts.error("The bookmarks file can't be opened");
            }
        });
        btnBookmarks.setAccelerator(new KeyCodeCombination(KeyCode.B, KeyCodeCombination.CONTROL_DOWN));
    }

    /**
     * Method to disable the UI Elements on the left of the GUI.
     * @param isDisabled boolean, true disables the left anchor panel.
     */
    private void disableGraphUIElements(boolean isDisabled) {
        anchorLeftControlPanel.setDisable(isDisabled);
    }

    /**
     * Initializes the buttons on the panel on the left side which do translation/zoom.
     */
    private void initLeftControlpanelScreenModifiers() {
        disableGraphUIElements(true);

        btnTranslateReset.setOnAction(event -> {
            grpDrawArea.setTranslateX(graphController.getLocationCenterX());
            grpDrawArea.setTranslateY(graphController.getLocationCenterY());
        });

        btnZoomReset.setOnAction(event -> anchorGraphPanel.getTransforms().clear());
    }

    /**
     * Initializes the button on the left side that are used to draw the graph.
     */
    private void initLeftControlpanelDraw() {
        disableGraphUIElements(true);

        btnDraw.setOnAction(e -> this.draw());

        btnDrawRandom.setOnAction(event -> {
            int randomNodeID = (new Random()).nextInt(this.graphController.getGraph().size());
            txtCenterNode.setText(Integer.toString(randomNodeID));
            this.draw();
        });

        btnBookmark.setOnAction(event -> buttonBookmark());

        txtMaxDrawDepth.textProperty().addListener(new NumbersOnlyListener(txtMaxDrawDepth));
        txtMaxDrawDepth.setText(INITIAL_MAX_DRAW_DEPTH);

        txtCenterNode.textProperty().addListener(new NumbersOnlyListener(txtCenterNode));
        txtCenterNode.setText(INITIAL_CENTER_NODE);
    }

    /**
     * Draw the current graph with current center node and depth settings.
     */
    public void draw() {
        Console.println("[%s] Drawing graph...", Thread.currentThread().getName());
        int centerNode = 0;
        int maxDepth = 0;
        try {
            centerNode = Integer.parseInt(txtCenterNode.getText());
            try {
                maxDepth = Integer.parseInt(txtMaxDrawDepth.getText());
            } catch (NumberFormatException e) {
                Alerts.warning("Radius is not a number, try again with a number as input.");
            }
        } catch (NumberFormatException e) {
            Alerts.warning("Center node ID is not a number, try again with a number as input.");
        }

        if (graphController.getGraph().contains(centerNode)) {
            this.graphController.clear();
            this.graphController.draw(centerNode, maxDepth);
            Console.println("[%s] Graph drawn.", Thread.currentThread().getName());
        } else {
            Alerts.warning("The centernode is not a existing node, "
                    + "try again with a number that exists as a node.");
        }
    }

    /**
     * {@link ChangeListener} to make a {@link TextField} only accept numbers.
     */
    private class NumbersOnlyListener implements ChangeListener<String> {
        private final TextField tf;

        /**
         * Constructor for the Listener.
         * @param tf {@link TextField} is the text field on which the listener listens
         */
        NumbersOnlyListener(TextField tf) {
            this.tf = tf;
        }

        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            if (!newValue.matches("\\d")) {
                tf.setText(newValue.replaceAll("[^\\d]", ""));
            }
        }
    }

    /**
     * Handles the events of the bookmark button.
     */
    private void buttonBookmark() {
        try {
            FXMLLoader loader = new FXMLLoader(ProgrammingLife.class.getResource("/CreateBookmarkWindow.fxml"));
            AnchorPane page = loader.load();
            GuiCreateBookmarkController gc = loader.getController();
            gc.setGuiController(this);
            gc.setText(txtCenterNode.getText(), txtMaxDrawDepth.getText());
            Scene scene = new Scene(page);
            Stage bookmarkDialogStage = new Stage();
            bookmarkDialogStage.setResizable(false);
            bookmarkDialogStage.setScene(scene);
            bookmarkDialogStage.setTitle("Create Bookmark");
            bookmarkDialogStage.initOwner(ProgrammingLife.getStage());
            bookmarkDialogStage.showAndWait();
        } catch (IOException e) {
            Alerts.error("This bookmark cannot be created.");
        }
    }

    /**
     * Initialises the mouse events.
     */
    private void initMouse() {
        anchorGraphPanel.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
            orgSceneX = event.getSceneX();
            orgSceneY = event.getSceneY();
            orgTranslateX = grpDrawArea.getTranslateX();
            orgTranslateY = grpDrawArea.getTranslateY();
            event.consume();
        });
        anchorGraphPanel.addEventHandler(MouseEvent.MOUSE_DRAGGED, event -> {
            grpDrawArea.setTranslateX(orgTranslateX + event.getSceneX() - orgSceneX);
            grpDrawArea.setTranslateY(orgTranslateY + event.getSceneY() - orgSceneY);
            event.consume();
        });
        anchorGraphPanel.addEventHandler(ScrollEvent.SCROLL, event -> {
            zoom(event.getSceneX(), event.getSceneY(), event.getDeltaX(), event.getDeltaY(), ZOOM_FACTOR);
        });
    }

    /**
     * Handles the zooming in and out of the group.
     * @param sceneX double for the x location.
     * @param sceneY double for the y location.
     * @param deltaX double for the x scale.
     * @param deltaY double for the y scale.
     * @param delta double the factor by which is zoomed.
     */
    private void zoom(double sceneX, double sceneY, double deltaX, double deltaY, double delta) {
        double scaleX = grpDrawArea.getScaleX();
        double scaleY = grpDrawArea.getScaleY();

        if (deltaX < 0 || deltaY < 0) {
            scaleX /= delta;
            scaleY /= delta;
        } else {
            scaleX *= delta;
            scaleY *= delta;
        }
        scaleX = clamp(scaleX, MIN_SCALE, MAX_SCALE);
        scaleY = clamp(scaleY, MIN_SCALE, MAX_SCALE);

        Scale scaleTransform = new Scale(scaleX, scaleY);

        scaleTransform.transform(deltaX, deltaY);
        scaleTransform.setPivotX(sceneX);
        scaleTransform.setPivotY(sceneY);

        anchorGraphPanel.getTransforms().add(scaleTransform);
    }

    /**
     * Clamp function used for zooming in and out.
     * @param value double current scale.
     * @param min double min scale value.
     * @param max double max scale value.
     * @return double scale value.
     */
    private static double clamp(double value, double min, double max) {

        if (Double.compare(value, min) < 0) {
            return min;
        }
{'fraction_non_alphanumeric': 0.09842209072978304, 'fraction_numerical': 0.0010143702451394759, 'mean_word_length': 2.2296842296842296, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 8}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '677681', 'n_tokens_mistral': 9478, 'n_tokens_neox': 9180, 'n_words': 2329}