diff --git a/src/main/java/edu/ntnu/idatt2003/controller/MainPageController.java b/src/main/java/edu/ntnu/idatt2003/controller/MainPageController.java index 0ba835e2c06a4219a5adc968ffadcedb99cf3dee..d8fdeea82ca86ade8da6993d8e23822dc1228180 100644 --- a/src/main/java/edu/ntnu/idatt2003/controller/MainPageController.java +++ b/src/main/java/edu/ntnu/idatt2003/controller/MainPageController.java @@ -1,31 +1,37 @@ package edu.ntnu.idatt2003.controller; +import edu.ntnu.idatt2003.model.AffineTransform2D; import edu.ntnu.idatt2003.model.ChaosGame; import edu.ntnu.idatt2003.model.ChaosGameDescription; import edu.ntnu.idatt2003.model.ChaosGameDescriptionFactory; import edu.ntnu.idatt2003.model.ChaosGameFileHandler; import edu.ntnu.idatt2003.model.Complex; import edu.ntnu.idatt2003.model.JuliaTransform; +import edu.ntnu.idatt2003.model.Matrix2x2; import edu.ntnu.idatt2003.model.Transform2D; import edu.ntnu.idatt2003.model.Vector2d; import edu.ntnu.idatt2003.view.MainPageView; -import java.util.ArrayList; -import java.util.List; + import java.io.File; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.io.FileNotFoundException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.InputMismatchException; +import java.util.List; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; +import java.util.stream.DoubleStream; +import java.util.stream.Stream; /** * The controller class for the main page of the ChaosGame application. @@ -35,7 +41,8 @@ import java.util.logging.SimpleFormatter; public class MainPageController { private final ChaosGame game; private final MainPageView view; - private List<String> customTransformations = new ArrayList<>(); + private final List<String> customTransformations; + private boolean addingCustomTransformation; private static final String TRANSFORMATIONS_PATH = "src/main/resources/transformations/"; private static final String SERIALIZED_GAME_PATH = "src/main/resources/savedTransformation.ser"; private static final Logger LOGGER = Logger.getLogger(MainPageController.class.getName()); @@ -64,6 +71,8 @@ public class MainPageController { this.game = loadGameState(); this.view = new MainPageView(this); this.game.registerObserver(view); + this.customTransformations = new ArrayList<>(getAllCustomTransforms()); + this.addingCustomTransformation = false; this.view.render(); Runtime.getRuntime().addShutdownHook(new Thread(this::saveGameState)); LOGGER.log(Level.INFO, "MainPageController initialized successfully."); @@ -87,6 +96,64 @@ public class MainPageController { return game; } + public double[] getMinCoordsX() { + return game.getMinCoordsList(); + } + + public double[] getMaxCoordsX() { + return game.getMaxCoordsList(); + } + + public String getCurrentTransformationName() { + return game.getDescriptionName(); + } + + public boolean isAddingCustomTransformation() { + return addingCustomTransformation; + } + /** + * Get the list of coordinate-arrays of the game. + * + * @return the list of coordinate-arrays of the game. + */ + public List<double[]> getTransformList() { + if (transformationIsJulia()) { + return getTransformListJulia(); + } else { + return getTransformListAffine(); + } + } + + private List<double[]> getTransformListJulia() { + List<double[]> transformList = new ArrayList<>(); + transformList.add(((JuliaTransform) game.getTransformList().get(0)).getPointAsList()); + return transformList; + } + + private List<double[]> getTransformListAffine() { + List<double[]> transformList = new ArrayList<>(); + for (Transform2D transform : game.getTransformList()) { + transformList.add(DoubleStream.concat(DoubleStream.of(((AffineTransform2D) transform) + .getMatrixCoordsList()), DoubleStream.of(((AffineTransform2D) transform) + .getVectorCoordsList())).toArray()); + } + return transformList; + } + + /** + * Check if the current transformation is a Julia set. If it is, + * return true and false otherwise. + * + * @return true if the transformation is a Julia set, false otherwise. + */ + public boolean transformationIsJulia() { + try { + return game.getTransformList().get(0) instanceof JuliaTransform; + } catch (IndexOutOfBoundsException e) { + return false; + } + } + /** * Run the chaos game simulation for the specified number of steps. If * the number of steps is negative, the canvas will be cleared. @@ -98,6 +165,24 @@ public class MainPageController { LOGGER.log(Level.INFO, "Chaos game simulation ran {0} steps successfully.", steps); } + /** + * Run the chaos game simulation for the specified number of steps. If + * the number of steps is negative, the canvas will be cleared. If the + * input cant be converted to an integer, an alert will be shown. And + * no steps will be run. + * + * @param steps The number of steps to run the simulation. + */ + public void runCustomSteps(String steps) { + try { + int stepsInt = Integer.parseInt(steps); + runSteps(stepsInt); + } catch (NumberFormatException e) { + view.showAlert("Invalid input. Please enter a valid integer."); + LOGGER.log(Level.WARNING, "Invalid input. Chaos game simulation was not run."); + } + } + /** * Validates the file that is uploaded., and calls storeFile if the * file exists and is formatted correctly. @@ -107,20 +192,23 @@ public class MainPageController { */ public void uploadFile(File file) { LOGGER.log(Level.INFO, "Uploading file: {0}", file.getName()); - validateFile(file); - if (!Files.exists(Path.of(TRANSFORMATIONS_PATH + file.getName())) - || view.askConfirmation("File already exists. Do you want to overwrite it?")) { + if (validateFile(file) + && (!Files.exists(Path.of(TRANSFORMATIONS_PATH + file.getName())) + || view.askConfirmation("File already exists. Do you want to overwrite it?"))) { storeFile(file); + LOGGER.log(Level.INFO, "File {0} uploaded successfully.", file.getName()); + view.showAlert("File " + file.getName() + " uploaded successfully."); } - } - private void validateFile(File file) { + private boolean validateFile(File file) { try { new ChaosGameFileHandler().readFromFile(file); + return true; } catch (InputMismatchException | FileNotFoundException e) { view.showAlert(e.getMessage()); LOGGER.log(Level.WARNING, "Error uploading file. File was not uploaded."); + return false; } } @@ -149,16 +237,36 @@ public class MainPageController { */ public void changeTransformation(ChaosGameDescriptionFactory .descriptionTypeEnum descriptionType) { - game.changeTransformation(descriptionType); + addingCustomTransformation = false; + game.changeTransformation(ChaosGameDescriptionFactory.get(descriptionType), + descriptionType.toString()); + LOGGER.log(Level.INFO, "Transformation was changed successfully to {0}", - descriptionType); + descriptionType); + } + + /** + * Changes the current transformation based on the given custom name. + * + * @param customName the name of the custom transformation to be applied + */ + public void changeTransformation(String customName) { + if (customName.equalsIgnoreCase("add new")) { + addingCustomTransformation = true; + this.view.render(); + } else { + addingCustomTransformation = false; + game.changeTransformation(ChaosGameDescriptionFactory.getCustom(customName), customName); + LOGGER.log(Level.INFO, "Transformation was changed successfully to {0}", + customName); + } } private void saveGameState() { LOGGER.log(Level.INFO, "Saving game state."); game.removeObserver(view); try (ObjectOutputStream oos = new ObjectOutputStream( - new FileOutputStream(SERIALIZED_GAME_PATH))) { + new FileOutputStream(SERIALIZED_GAME_PATH))) { oos.writeObject(game); LOGGER.log(Level.INFO, "Game state saved successfully in {0}", SERIALIZED_GAME_PATH); } catch (IOException e) { @@ -187,51 +295,162 @@ public class MainPageController { } else { LOGGER.log(Level.WARNING, "No saved game state found. Creating new game."); } - return new ChaosGame(ChaosGameDescriptionFactory + ChaosGame newGame = new ChaosGame(ChaosGameDescriptionFactory .get(ChaosGameDescriptionFactory.descriptionTypeEnum.SIERPINSKI_TRIANGLE), - 600, 600); + 650, 650); + newGame.setDescriptionName(ChaosGameDescriptionFactory.descriptionTypeEnum + .SIERPINSKI_TRIANGLE.toString()); + return newGame; } /** - * Changes the current custom transformation based on the given custom name. + * Adds a new custom transformation with the specified parameters and writes it to a file. * - * @param customName the name of the custom transformation to be applied + * @param minCoords the minimum coordinates for the transformation + * @param maxCoords the maximum coordinates for the transformation + * @param transform the list of 2D transformations to be applied + * @param transformationName the name of the custom transformation */ - public void changeCustomTransformation(String customName) { - ChaosGameDescription chaosGameDescription = customNameHandle(customName); - game.changeCustomTransformation(chaosGameDescription); + public void addCustomTransformation(String[] minCoords, String[] maxCoords, + List<String[]> transform, String transformationName) { + ChaosGameFileHandler chaosGameFileHandler = new ChaosGameFileHandler(); + try { + ChaosGameDescription newChaosGameDescription = + new ChaosGameDescription( + getVector2dFromStringList(minCoords), + getVector2dFromStringList(maxCoords), + getTransformListFromStringList(transform) + ); + if (!Files.exists(Path.of(TRANSFORMATIONS_PATH + transformationName + ".txt")) + || view.askConfirmation("Custom transformation with the same name already exists. " + + "Do you want to overwrite it?")) { + chaosGameFileHandler + .writeToFile(newChaosGameDescription, + TRANSFORMATIONS_PATH + transformationName + ".txt"); + customTransformations.add(transformationName); + view.render(); + view.showAlert("Custom transformation " + transformationName + " added successfully."); + } + + } catch (IllegalArgumentException e) { + view.showAlert(e.getMessage()); + } + + } /** - * Retrieves the ChaosGameDescription associated with the given custom name. + * Creates a Vector2d object from a string array, containing the x and y coordinates. * - * @param customName the name of the custom transformation - * @return the ChaosGameDescription corresponding to the custom name + * @param vector the string array containing the x and y coordinates + * @return the Vector2d object created from the string array */ + private Vector2d getVector2dFromStringList(String[] vector) { + try { + return new Vector2d(Double.parseDouble(vector[0]), Double.parseDouble(vector[1])); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid coordinates. Please enter valid integers."); + } + } + + /** + * Creates a list of Transform2D objects from a list of string arrays. + * + * @param transform the list of string arrays containing the transformation parameters + * @return the list of Transform2D objects created from the string arrays + */ + private List<Transform2D> getTransformListFromStringList(List<String[]> transform) + throws NumberFormatException { + try { + List<Transform2D> transformList = new ArrayList<>(); + for (String[] transformation : transform) { + if (transformation.length == 2) { + transformList.addAll(parseJuliaTransform(transformation)); + } else if (transformation.length == 6) { + transformList.add(parseAffineTransform(transformation)); + } + } + return transformList; + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid coordinates. Please " + + "enter valid decimal numbers."); + } - public ChaosGameDescription customNameHandle(String customName) { - return ChaosGameDescriptionFactory.getCustom(customName); } /** - * Adds a new custom transformation with the specified parameters and writes it to a file. + * Parses the Julia transformations and returns a List of Julia Transformations. * - * @param minCoords the minimum coordinates for the transformation - * @param maxCoords the maximum coordinates for the transformation - * @param transform the list of 2D transformations to be applied - * @param transformationName the name of the custom transformation + * @param transformation the string array containing the transformation parameters + * @return the list of Julia Transformations */ + private List<Transform2D> parseJuliaTransform(String[] transformation) { + return List.of( + new JuliaTransform( + new Complex( + Double.parseDouble(transformation[0]), + Double.parseDouble(transformation[1]) + ), + 1), + new JuliaTransform( + new Complex( + Double.parseDouble(transformation[0]), + Double.parseDouble(transformation[1]) + ), + 1) + ); + } - public void addCustomTransformation(Vector2d minCoords, Vector2d maxCoords, - List<Transform2D> transform, String transformationName) { - ChaosGameFileHandler chaosGameFileHandler = new ChaosGameFileHandler(); - ChaosGameDescription newChaosGameDescription = - new ChaosGameDescription(minCoords, maxCoords, transform); - chaosGameFileHandler - .writeToFile(newChaosGameDescription, TRANSFORMATIONS_PATH + transformationName + ".txt"); - customTransformations.add(transformationName); - view.render(); + /** + * Parses the Affine transformations and returns a List of Julia Transformations. + * + * @param transformation the string array containing the transformation parameters + * @return the list of Affine Transformations + */ + private AffineTransform2D parseAffineTransform(String[] transformation) { + return new AffineTransform2D( + new Matrix2x2( + Double.parseDouble(transformation[0]), + Double.parseDouble(transformation[1]), + Double.parseDouble(transformation[2]), + Double.parseDouble(transformation[3]) + ), + new Vector2d( + Double.parseDouble(transformation[4]), + Double.parseDouble(transformation[5]) + )); + + } + + /** + * Retrieves a list of all custom transformation files in the transformations directory + * and updates the customTransformations list.* + * + * @return the updated list of custom transformation file names. + */ + public List<String> getAllCustomTransforms() { + List<String> transformations = new ArrayList<>(); + Path transformationsPath = Paths.get(TRANSFORMATIONS_PATH); + + if (Files.exists(transformationsPath) && Files.isDirectory(transformationsPath)) { + try (Stream<Path> paths = Files.list(transformationsPath)) { + transformations = paths + .filter(Files::isRegularFile) + .map(Path::getFileName) + .map(Path::toString) + .map(name -> name.replace(".txt", "")) + .toList(); + LOGGER.log(Level.INFO, "All custom transformations retrieved successfully."); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Error retrieving custom transformation files.", e); + } + } else { + LOGGER.log(Level.WARNING, "Transformations directory does not exist or is not a " + + "directory."); + } + + return transformations; } /** diff --git a/src/main/java/edu/ntnu/idatt2003/model/AffineTransform2D.java b/src/main/java/edu/ntnu/idatt2003/model/AffineTransform2D.java index 35d5520f4b2cee57bfb19fdbd9a89c2785e34289..88cc39d9bbd9aff4118ac9cc3f3522a58bb77b3d 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/AffineTransform2D.java +++ b/src/main/java/edu/ntnu/idatt2003/model/AffineTransform2D.java @@ -46,6 +46,12 @@ public class AffineTransform2D implements Transform2D, Serializable { return matrix.multiply(point).add(vector); } + public double[] getMatrixCoordsList() { + return matrix.getCoordsList(); + } + public double[] getVectorCoordsList() { + return vector.getCoordsList(); + } /** * Returns a string representation of the AffineTransform2D, by combining the strings * of the matrix and vector separated by a ','. diff --git a/src/main/java/edu/ntnu/idatt2003/model/ChaosGame.java b/src/main/java/edu/ntnu/idatt2003/model/ChaosGame.java index 14bd40bf85ed3f46dd2a2859d56c0548ee0919db..c977f328a9367261b908cbd8407c342303bab75c 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/ChaosGame.java +++ b/src/main/java/edu/ntnu/idatt2003/model/ChaosGame.java @@ -16,6 +16,7 @@ public class ChaosGame implements Serializable { private ChaosCanvas canvas; private ChaosGameDescription description; + private String descriptionName; private Vector2d currentPoint; private final int width; private final int height; @@ -35,6 +36,7 @@ public class ChaosGame implements Serializable { this.observers = new ArrayList<>(); this.width = width; this.height = height; + this.descriptionName = ""; setDescription(description); randomGenerator = new Random(); this.totalSteps = 0; @@ -45,26 +47,84 @@ public class ChaosGame implements Serializable { * * @return The canvas of the chaos game. */ - public ChaosCanvas getCanvas() { return canvas; } + /** + * Returns the minimum coordinates of the chaos game. + * + * @return The minimum coordinates of the chaos game. + */ + public double[] getMinCoordsList() { + return description.getMinCoordsList(); + } + + /** + * Returns the maximum coordinates of the chaos game. + * + * @return The maximum coordinates of the chaos game. + */ + public double[] getMaxCoordsList() { + return description.getMaxCoordsList(); + } + + /** + * Returns the list of transformations of the chaos game. + * + * @return The list of transformations of the chaos game. + */ + public List<Transform2D> getTransformList() { + return description.getTransform(); + } + + /** + * Returns the total number of steps run in the chaos game. + * + * @return The total number of steps run in the chaos game. + */ public int getTotalSteps() { return totalSteps; } + + public String getDescriptionName() { + return descriptionName; + } + + public void setDescriptionName(String descriptionName) { + this.descriptionName = descriptionName; + } + + /** + * Runs the chaos game simulation for the specified number of steps, by + * calling the runSteps-method and passing true as the second parameter + * to also update totalSteps. + * + * @param steps The number of steps to run the simulation. + */ public void runStepsAndUpdateTotal(int steps) { runSteps(steps, true); } + + /** + * Runs the chaos game simulation for the specified number of steps, by + * calling the runSteps-method and passing false as the second parameter + * to not update totalSteps. + * + * @param steps The number of steps to run the simulation. + */ public void runStepsWithoutUpdatingTotal(int steps) { runSteps(steps, false); } + /** - * Runs the chaos game simulation for the specified number of steps. - * Generates points on the canvas based on random chosen transformation. - * The current point is replaced with the new, which gets transformed by a random transformation. + * Runs the chaos game simulation for the specified number of steps or cleared + * if steps is negative. Generates points on the canvas based on random chosen + * transformation. If addSteps is true, the totalSteps is updated, if false, + * the totalSteps is not updated. In the end, the observers are notified. * - * @param steps The number of steps to run the simulation. + * @param steps The number of steps to run the simulation. + * @param addSteps Whether to update the totalSteps or not. */ private void runSteps(int steps, boolean addSteps) { if (steps < 0) { @@ -81,6 +141,10 @@ public class ChaosGame implements Serializable { notifyObservers(); } + /** + * Generates a point on the canvas based on a random transformation, and puts + * the point on the canvas. It also updates the current point to the new point. + */ private void applyRandomTransformation() { int randomIndex = randomGenerator.nextInt(description.getTransform().size()); currentPoint = description.getTransform().get(randomIndex).transform(currentPoint); @@ -91,15 +155,13 @@ public class ChaosGame implements Serializable { * Changes the transformation of the chaos game. Calls the setDescription-method * and notifies the observers that it has changed. * - * @param descriptionType The type of fractal description to retrieve. + * @param chaosGameDescription The type of fractal description to retrieve. */ - public void changeTransformation(ChaosGameDescriptionFactory - .descriptionTypeEnum descriptionType) { - setDescription(ChaosGameDescriptionFactory.get(descriptionType)); - notifyObservers(); - } - public void changeCustomTransformation(ChaosGameDescription customDescription) { - setDescription(customDescription); + public void changeTransformation(ChaosGameDescription chaosGameDescription, + String descriptionName) { + setDescription(chaosGameDescription); + setDescriptionName(descriptionName); + totalSteps = 0; notifyObservers(); } diff --git a/src/main/java/edu/ntnu/idatt2003/model/ChaosGameDescription.java b/src/main/java/edu/ntnu/idatt2003/model/ChaosGameDescription.java index 877d9e5b75e77699c87239104147246c7a133693..1ac83190e11ed57c7cde292419bd7977c2cdf77f 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/ChaosGameDescription.java +++ b/src/main/java/edu/ntnu/idatt2003/model/ChaosGameDescription.java @@ -54,6 +54,12 @@ public class ChaosGameDescription implements Serializable { return maxCoords; } + public double[] getMinCoordsList() { + return minCoords.getCoordsList(); + } + public double[] getMaxCoordsList() { + return maxCoords.getCoordsList(); + } /** * Get the list of transformations to be used. * diff --git a/src/main/java/edu/ntnu/idatt2003/model/JuliaTransform.java b/src/main/java/edu/ntnu/idatt2003/model/JuliaTransform.java index 0ed9503bec436190bd53b27b5387c0757c865a40..47a342b4d1050328cd8beddcb7fb304153825378 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/JuliaTransform.java +++ b/src/main/java/edu/ntnu/idatt2003/model/JuliaTransform.java @@ -32,6 +32,9 @@ public class JuliaTransform implements Transform2D, Serializable { this.sign = sign; } + public double[] getPointAsList() { + return new double[]{point.getX0(), point.getX1()}; + } /** * Transforms a 2D vector by taking adding/subtracting based on the sign * and taking the square root. diff --git a/src/main/java/edu/ntnu/idatt2003/model/Matrix2x2.java b/src/main/java/edu/ntnu/idatt2003/model/Matrix2x2.java index 139a41fe61be2de8a28ac0dc357c646e3cbde9f8..fab4921812b3d03a447a64ababc990de1e40fe45 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/Matrix2x2.java +++ b/src/main/java/edu/ntnu/idatt2003/model/Matrix2x2.java @@ -42,6 +42,9 @@ public class Matrix2x2 implements Serializable { double x1 = a10 * vector.getX0() + a11 * vector.getX1(); return new Vector2d(x0, x1); } + public double[] getCoordsList() { + return new double[]{a00, a01, a10, a11}; + } /** * Returns a string representation of the Matrix2x2, that separates the diff --git a/src/main/java/edu/ntnu/idatt2003/model/Vector2d.java b/src/main/java/edu/ntnu/idatt2003/model/Vector2d.java index b6b449797a71dd80cb9f5dd0863204b5bd62e68e..04b78c8b98f11eeae65902240095d37fcf81f59c 100644 --- a/src/main/java/edu/ntnu/idatt2003/model/Vector2d.java +++ b/src/main/java/edu/ntnu/idatt2003/model/Vector2d.java @@ -51,6 +51,9 @@ public class Vector2d implements Serializable { return x1; } + public double[] getCoordsList() { + return new double[]{x0, x1}; + } /** * Adds another vector to this vector. * diff --git a/src/main/java/edu/ntnu/idatt2003/view/MainPageView.java b/src/main/java/edu/ntnu/idatt2003/view/MainPageView.java index 1c666cb8de2deb679fad65ba0c3a3195b7fe7ef6..19c2a003e018dfbcba5992efd0a061a09e04a7b1 100644 --- a/src/main/java/edu/ntnu/idatt2003/view/MainPageView.java +++ b/src/main/java/edu/ntnu/idatt2003/view/MainPageView.java @@ -1,33 +1,31 @@ package edu.ntnu.idatt2003.view; import edu.ntnu.idatt2003.controller.MainPageController; -import edu.ntnu.idatt2003.model.AffineTransform2D; import edu.ntnu.idatt2003.model.ChaosGameDescriptionFactory; import edu.ntnu.idatt2003.model.ChaosGameObserver; -import edu.ntnu.idatt2003.model.Complex; -import edu.ntnu.idatt2003.model.JuliaTransform; -import edu.ntnu.idatt2003.model.Matrix2x2; -import edu.ntnu.idatt2003.model.Transform2D; -import edu.ntnu.idatt2003.model.Vector2d; import edu.ntnu.idatt2003.utils.Sizes; +import edu.ntnu.idatt2003.view.components.ChaosImage; +import edu.ntnu.idatt2003.view.components.StyledButton; +import edu.ntnu.idatt2003.view.components.StyledComboBox; +import edu.ntnu.idatt2003.view.components.StyledTextField; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Objects; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; -import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; @@ -42,16 +40,14 @@ import javafx.stage.Stage; */ public class MainPageView extends Scene implements ChaosGameObserver { private final BorderPane root; - private final StackPane pageContainer; private final MainPageController controller; - private static final int PAGE_CONTAINER_MARGIN = 50; - private static final int BUTTON_COUNT = 6; - private static final int BUTTON_HEIGHT = 40; - private static final int BUTTON_WIDTH = (int) (Sizes.SCREEN_WIDTH - PAGE_CONTAINER_MARGIN) - / BUTTON_COUNT; - private static TextField xField; - private static TextField yField; - + private static final int BUTTON_COUNT = 7; + private static final int COMPONENT_HEIGHT = 40; + private static final int BUTTON_WIDTH = (int) (Sizes.SCREEN_WIDTH) / BUTTON_COUNT; + private static final int DEFAULT_SPACING = 10; + private TextField x0Field; + private TextField x1Field; + private TransformationType selectedTransformation; /** @@ -63,9 +59,11 @@ public class MainPageView extends Scene implements ChaosGameObserver { super(new BorderPane(), Sizes.SCREEN_WIDTH, Sizes.SCREEN_HEIGHT); this.getStylesheets().add(Objects.requireNonNull(getClass() .getResource("/styles/mainPage.css")).toExternalForm()); + this.getStylesheets().add(Objects.requireNonNull(getClass() + .getResource("/styles/components.css")).toExternalForm()); root = (BorderPane) this.getRoot(); - pageContainer = new StackPane(); + this.selectedTransformation = TransformationType.AFFINE; this.controller = mainPageController; } @@ -75,8 +73,8 @@ public class MainPageView extends Scene implements ChaosGameObserver { */ public void render() { root.getStyleClass().add("main-page"); - pageContainer.getChildren().clear(); - createPageContainer(); + root.getChildren().clear(); + createPage(); } /** @@ -87,22 +85,140 @@ public class MainPageView extends Scene implements ChaosGameObserver { } /** - * Creates the page container with a button container. - * The button container contains 7 buttons. + * Creates the page with a button container at the top and an edit panel, + * dynamic julia container and the canvas at the bottom. + */ + private void createPage() { + root.setTop(createButtonContainer()); + root.setCenter(createCenterContainer()); + } + + /** + * Creates the center container with the edit panel and the dynamic julia container. + * + * @return The center container. + */ + private HBox createCenterContainer() { + HBox centerContainer = new HBox(DEFAULT_SPACING); + centerContainer.getStyleClass().add("center-container"); + centerContainer.getChildren().addAll( + createEditPanelAndDynamicJuliaContainer(), + ChaosImage.createImageFromCanvas(controller.getGame().getCanvas()) + ); + return centerContainer; + } + + /** + * Creates the edit panel and the dynamic julia container. + * + * @return The edit panel and the dynamic julia container. + */ + private VBox createEditPanelAndDynamicJuliaContainer() { + VBox editPanelAndDynamicJuliaContainer = new VBox(DEFAULT_SPACING); + HBox.setHgrow(editPanelAndDynamicJuliaContainer, Priority.ALWAYS); + + editPanelAndDynamicJuliaContainer.getStyleClass().add("canvas-and-dynamic-julia-container"); + editPanelAndDynamicJuliaContainer.getChildren().addAll( + createAddTransformationPanel(), + createVboxSpacing(), + createDynamicJuliaContainer() + ); + return editPanelAndDynamicJuliaContainer; + } + + /** + * Creates the dynamic julia container with the julia information and the mouse box. + * + * @return The dynamic julia container. + */ + private HBox createDynamicJuliaContainer() { + HBox dynamicJuliaContainer = new HBox(DEFAULT_SPACING); + dynamicJuliaContainer.setAlignment(Pos.BOTTOM_CENTER); + x0Field = new StyledTextField("x: ", 100, 20); + x1Field = new StyledTextField("y: ", 100, 20); + if (controller.transformationIsJulia()) { + VBox juliaInformationContainer = new VBox(DEFAULT_SPACING); + HBox.setHgrow(juliaInformationContainer, Priority.ALWAYS); + juliaInformationContainer.getChildren().addAll( + createTextBox("Total steps: " + controller.getGame().getTotalSteps()), + new HBox(10, createTextBox("X-value: "), x0Field), + new HBox(10, createTextBox("Y-value: "), x1Field) + ); + dynamicJuliaContainer.getChildren().addAll( + juliaInformationContainer, + mouseBox() + ); + } + return dynamicJuliaContainer; + } + + /** + * Creates a Region that fills the remaining space in a VBoc. + * + * @return The Region that fills the VBox. + */ + private Region createVboxSpacing() { + Region spacing = new Region(); + VBox.setVgrow(spacing, Priority.ALWAYS); + return spacing; + } + + /** + * Creates a text box with the specified text. The text box is styled with the + * "text-box" style class. The text box is a StackPane with a Label as a child. + * The text box is configured to grow horizontally. + * + * @param text The text to display in the text box. + * @return The text box. + */ + private StackPane createTextBox(String text) { + StackPane textBox = new StackPane(); + textBox.getStyleClass().add("text-box"); + HBox.setHgrow(textBox, Priority.ALWAYS); + textBox.getChildren().add(new Label(text)); + return textBox; + } + + /** + * Creates a HBox containing a text box with the specified text and as many text fields + * as specified is given prompt text to, which gets the size given as parameters. + * + * @param text The text to display in the text box. + * @param width The width of the text field. + * @param height The height of the text field. + * @param promptTexts The prompt texts for the text fields. + * @return The HBox containing the text box and text fields. + */ + private HBox createTextBoxWithTextField(String text, int width, + int height, String... promptTexts) { + HBox container = new HBox(DEFAULT_SPACING); + container.getChildren().add(createTextBox(text)); + for (String promptText : promptTexts) { + container.getChildren().add(new StyledTextField(promptText, width, height)); + } + return container; + } + + /** + * Creates a HBox containing a text box with the specified text and as many text fields + * as the length of the doubles array, which gets the height and with given as parameters. + * + * @param text The text to display in the text box. + * @param width The width of the text field. + * @param height The height of the text field. + * @param coords The text for the StyledTextField. + * @return The HBox containing the text box and text fields. */ - private void createPageContainer() { - HBox contentContainer = new HBox( - createAddTransformationPanel(), - ChaosImage.createImageFromCanvas(controller.getGame().getCanvas()), - mouseBox() - ); - pageContainer.getChildren().addAll(contentContainer, createButtonContainer()); - pageContainer.getStyleClass().add("page-container"); - pageContainer.setMaxHeight(Sizes.SCREEN_HEIGHT - PAGE_CONTAINER_MARGIN); - pageContainer.setMaxWidth(Sizes.SCREEN_WIDTH - PAGE_CONTAINER_MARGIN); - root.setCenter(pageContainer); + private HBox createTextBoxWithTextField(String text, int width, int height, double[] coords) { + HBox container = new HBox(DEFAULT_SPACING); + container.getChildren().add(createTextBox(text)); + for (double coordinate : coords) { + container.getChildren().add(new StyledTextField(coordinate, width, height)); + } + return container; } + /** * Creates a button container with a ComboBox to change the type * of transformation, buttons for running steps/resetting, the @@ -112,7 +228,7 @@ public class MainPageView extends Scene implements ChaosGameObserver { */ private HBox createButtonContainer() { - HBox buttonContainer = new HBox(); + HBox buttonContainer = new HBox(DEFAULT_SPACING); buttonContainer.getStyleClass().add("button-container"); buttonContainer.setMaxHeight(Region.USE_PREF_SIZE); buttonContainer.setAlignment(Pos.CENTER); @@ -121,88 +237,51 @@ public class MainPageView extends Scene implements ChaosGameObserver { buttonContainer.getChildren().addAll( createComboBox(), createCustomComboBox(), - createButton(10), - createButton(100), - createButton(1000), - createInputField(), - createButton(-1) + new StyledButton("10 Steps", BUTTON_WIDTH, COMPONENT_HEIGHT, + e -> controller.runSteps(10)), + new StyledButton("100 Steps", BUTTON_WIDTH, COMPONENT_HEIGHT, + e -> controller.runSteps(100)), + new StyledButton("1000 Steps", BUTTON_WIDTH, COMPONENT_HEIGHT, + e -> controller.runSteps(1000)), + createStepsTextField(), + new StyledButton("Reset", BUTTON_WIDTH, COMPONENT_HEIGHT, + e -> controller.runSteps(-1)) ); return buttonContainer; } private ComboBox<ChaosGameDescriptionFactory.descriptionTypeEnum> createComboBox() { - ComboBox<ChaosGameDescriptionFactory.descriptionTypeEnum> transformMenu = new ComboBox<>(); - transformMenu.getStyleClass().add("combo-box"); - transformMenu.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT); - transformMenu.setPromptText("Transformation"); - - transformMenu.getItems().addAll( - ChaosGameDescriptionFactory.descriptionTypeEnum.values() - ); + StyledComboBox<ChaosGameDescriptionFactory.descriptionTypeEnum> transformMenu = + new StyledComboBox<>("Transformation", BUTTON_WIDTH, COMPONENT_HEIGHT, + Arrays.asList(ChaosGameDescriptionFactory.descriptionTypeEnum.values()) + ); transformMenu.setOnAction(e -> controller.changeTransformation(transformMenu.getValue())); - return transformMenu; } - private ComboBox<String> createCustomComboBox() { - ComboBox<String> customMenu = new ComboBox<>(); - customMenu.getStyleClass().add("combo-box"); - customMenu.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT); - customMenu.setPromptText("Custom Transformations"); - - List<String> customTransformations = controller.getCustomTransformation(); - - if (customTransformations != null && !customTransformations.isEmpty()) { - customMenu.getItems().addAll(customTransformations); - } - - customMenu.setOnAction(e -> controller.changeCustomTransformation(customMenu.getValue())); - - return customMenu; - } - - /** - * Creates a button with a specified number of steps. If -1 it's - * a reset button - * - * @param steps The number of steps for the button. - * @return The button. - */ - private Button createButton(int steps) { - Button button; - if (steps == -1) { - button = new Button("Reset"); - } else { - button = new Button("Steps: " + steps); - } - button.getStyleClass().add("button"); - button.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT); - - button.setOnAction(e -> controller.runSteps(steps)); - - return button; + private StyledTextField createStepsTextField() { + StyledTextField stepsField = new StyledTextField("Steps", BUTTON_WIDTH, COMPONENT_HEIGHT); + stepsField.getStyleClass().set(0, "button"); + stepsField.setOnAction(e -> { + controller.runCustomSteps(stepsField.getText()); + stepsField.clear(); + }); + return stepsField; } /** - * Creates an input field for custom number of steps. + * Creates a StyledComboBox with setOnAction to change the transformation based on + * the selected value. * - * @return The input field. + * @return The ComboBox with custom transformations. */ - private TextField createInputField() { - TextField inputField = new TextField(); - inputField.setPromptText("Steps"); - inputField.getStyleClass().add("input-field"); - inputField.setPrefSize(BUTTON_WIDTH, BUTTON_HEIGHT); - inputField.setOnAction(e -> { - try { - controller.runSteps(Integer.parseInt(inputField.getText())); - } catch (Exception ex) { - showAlert("Invalid input. Please enter an integer."); - inputField.clear(); - } - }); - return inputField; + private ComboBox<String> createCustomComboBox() { + StyledComboBox<String> customMenu = new StyledComboBox<>("Custom Transformations", + BUTTON_WIDTH, COMPONENT_HEIGHT, controller.getCustomTransformation()); + customMenu.getItems().add("Add new"); + customMenu.setOnAction(e -> controller.changeTransformation(customMenu.getValue())); + return customMenu; } /** @@ -259,181 +338,155 @@ public class MainPageView extends Scene implements ChaosGameObserver { */ public VBox createAddTransformationPanel() { VBox addPanel = createMainPanel(); - TextField transformationName = createTextField("transformation name"); - ComboBox<TransformationType> transformationComboBox = createTransformationComboBox(); - HBox transformationInputField = createTransformationInputField(transformationComboBox); - HBox startVectorField = createVectorHbox(Arrays.asList("x0", "y0")); - HBox endVectorField = createVectorHbox(Arrays.asList("x0", "y0")); + TextField transformationName = new StyledTextField("Transformation name", 210, 20); + transformationName.setText(controller.getCurrentTransformationName()); + ComboBox<TransformationType> transformationComboBox = createAddComboBox(); + HBox startVectorField; + HBox endVectorField; + if (controller.isAddingCustomTransformation()) { + startVectorField = createTextBoxWithTextField("Start vector:", 100, 20, + "x0", "x1"); + endVectorField = createTextBoxWithTextField("End vector:", 100, 20, + "x0", "x1"); + } else { + startVectorField = createTextBoxWithTextField("Min vector:", 100, 20, + controller.getMinCoordsX()); + endVectorField = createTextBoxWithTextField("Max vector:", 100, 20, + controller.getMaxCoordsX()); + } - addPanel.getChildren().addAll( - transformationName, - transformationComboBox, - transformationInputField, - startVectorField, - endVectorField, - createButton("Save", e -> saveTransformation( - transformationName, transformationComboBox, transformationInputField, startVectorField, - endVectorField)), - createButton("Cancel", e -> render()), - createButton("Add File", e -> uploadFile())); - StackPane.setAlignment(addPanel, Pos.BOTTOM_LEFT); + VBox transformationVbox = createTransformationVbox(transformationComboBox); - return addPanel; - } + addPanel.getChildren().addAll( + new HBox(DEFAULT_SPACING, + createTextBox("Transformation name:"), + transformationName + ), + new HBox(DEFAULT_SPACING, + createTextBox("Transformation type:"), + transformationComboBox + ), + startVectorField, + endVectorField, + transformationVbox, + new HBox( + DEFAULT_SPACING, + new StyledButton("Save", 20, + e -> saveTransformation(transformationName, transformationComboBox, + transformationVbox, startVectorField, endVectorField)), + new StyledButton("Cancel", 20, e -> render()), + new StyledButton("Add File", 100, 20, e -> uploadFile()) + ) - /** - * Creates a generic VBox panel with spacing and alignment. - * - * @return a VBox configured as the main panel. - */ - private VBox createMainPanel() { - VBox panel = new VBox(10); - panel.getStyleClass().add("add-panel"); - panel.setAlignment(Pos.CENTER); - return panel; - } + ); - /** - * Creates a ComboBox populated with transformation types. - * - * @return a ComboBox populated with transformation types. - */ - private ComboBox<TransformationType> createTransformationComboBox() { - ComboBox<TransformationType> comboBox = new ComboBox<>(); - comboBox.getItems().addAll(TransformationType.values()); - comboBox.setPromptText("Select Transformation"); - return comboBox; - } + StackPane.setAlignment(addPanel, Pos.BOTTOM_LEFT); - /** - * Enum representing the types of transformations. - */ - public enum TransformationType { - JULIA, AFFINE + return addPanel; } - /** - * Creates the transformation input field container and sets up its update behavior. - * - * @param transformationComboBox the ComboBox for selecting transformation types. - * @return an HBox containing the input fields for transformations. - */ - private HBox createTransformationInputField(ComboBox<TransformationType> transformationComboBox) { - HBox inputField = new HBox(10); - transformationComboBox.setOnAction( - e -> updateTransformationFields(transformationComboBox, inputField)); - updateTransformationFields(transformationComboBox, inputField); - return inputField; + private ComboBox<TransformationType> createAddComboBox() { + StyledComboBox<TransformationType> transformMenu = new StyledComboBox<>("Transformation", + 210, COMPONENT_HEIGHT, Arrays.asList(TransformationType.values())); + transformMenu.setOnAction(e -> { + selectedTransformation = transformMenu.getValue(); + controller.changeTransformation("add new"); + }); + if (controller.isAddingCustomTransformation()) { + transformMenu.setValue(this.selectedTransformation); + } else if (controller.transformationIsJulia()) { + transformMenu.setValue(TransformationType.JULIA); + } else { + transformMenu.setValue(TransformationType.AFFINE); + } + return transformMenu; } /** - * Updates the transformation input fields based on the selected transformation type. + * Creates a VBox containing input fields for transformations. * - * @param comboBox the ComboBox for selecting transformation types. - * @param inputField the HBox containing the input fields for transformations. + * @return a VBox configured with input TextFields. */ - private void updateTransformationFields(ComboBox<TransformationType> comboBox, HBox inputField) { - inputField.getChildren().clear(); - if (comboBox.getValue() != null) { - inputField.getChildren().add(createTransformationVbox( - comboBox.getValue() == TransformationType.AFFINE, - comboBox.getValue() == TransformationType.JULIA - ? Arrays.asList("Real part", "Imaginary part") : null)); + private VBox createTransformationVbox(ComboBox<TransformationType> transformationComboBox) { + VBox vbox = new VBox(10); + vbox.setFillWidth(true); + vbox.setAlignment(Pos.CENTER); + if (transformationComboBox.getValue() == TransformationType.AFFINE) { + vbox.getChildren().add( + new StyledButton("Add Transformation", 250, 20, + e -> vbox.getChildren().add(createTextBoxWithTextField("Transformation:", + 55, 20, "a00", "a01", "a10", "a11", "v0", "v1")) + ) + ); } - } + if (!controller.isAddingCustomTransformation()) { + for (double[] coords : controller.getTransformList()) { + vbox.getChildren().add(createTextBoxWithTextField("Transformation:", + 55, 20, coords)); + } + } else if (controller.isAddingCustomTransformation() + && transformationComboBox.getValue() == TransformationType.JULIA) { + vbox.getChildren().add(createTextBoxWithTextField("Transformation:", + 55, 20, "c0", "c1")); - /** - * Creates a TextField with the specified prompt text. - * - * @param promptText the prompt text for the TextField. - * @return a configured TextField. - */ - private TextField createTextField(String promptText) { - TextField textField = new TextField(); - textField.setPromptText(promptText); - return textField; - } + } - /** - * Creates a generic button with the specified label and action. - * - * @param label the label for the button. - * @param eventHandler the event handler for the button action. - * @return a configured Button. - */ - private Button createButton(String label, EventHandler<ActionEvent> eventHandler) { - Button button = new Button(label); - button.getStyleClass().add("button"); - button.setOnAction(eventHandler); - return button; + return vbox; } /** - * Creates a VBox containing input fields for transformations. + * Creates a generic VBox panel with spacing and alignment. * - * @param isAffine indicates if the VBox is for affine transformations. - * @param promptTexts list of prompt texts for the TextFields. - * @return a VBox configured with input TextFields. + * @return a VBox configured as the main panel. */ - private VBox createTransformationVbox(boolean isAffine, List<String> promptTexts) { - VBox vbox = new VBox(isAffine ? 5 : 10); - if (isAffine) { - vbox.getChildren().add(createButton("Add Transformation", - e -> vbox.getChildren().add(createVectorHbox( - Arrays.asList("X0", "Y0", "X1", "Y1", "V0", "V1"))))); - } else if (promptTexts != null) { - vbox.getChildren().add(createVectorHbox(promptTexts)); - } - return vbox; + private VBox createMainPanel() { + VBox panel = new VBox(10); + panel.getStyleClass().add("add-panel"); + panel.setAlignment(Pos.CENTER); + return panel; } /** - * Creates an HBox containing input fields for transformations. - * - * @param promptTexts list of prompt texts for the TextFields. - * @return an HBox configured with input TextFields. + * Enum representing the types of transformations. */ - private HBox createVectorHbox(List<String> promptTexts) { - HBox hbox = new HBox(10); - for (String promptText : promptTexts) { - hbox.getChildren().add(createTextField(promptText)); - } - return hbox; + public enum TransformationType { + JULIA, AFFINE } /** * Saves the transformation with the given parameters. * - * @param transformationName the name of the transformation. + * @param transformationName the name of the transformation. * @param transformationComboBox the ComboBox for selecting transformation types. - * @param transformationInputField the HBox containing the input fields for transformations. - * @param startVectorField the TextField for the start vector. - * @param endVectorField the TextField for the end vector. + * @param transformationVbox the HBox containing the input fields for transformations. + * @param startVectorField the TextField for the start vector. + * @param endVectorField the TextField for the end vector. */ private void saveTransformation(TextField transformationName, - ComboBox<TransformationType> transformationComboBox, HBox transformationInputField, - HBox startVectorField, HBox endVectorField) { - List<Transform2D> transformations = getInputInformation(transformationComboBox.getValue(), - transformationInputField); - Vector2d startVector = getInputVector(startVectorField); - Vector2d endVector = getInputVector(endVectorField); + ComboBox<TransformationType> transformationComboBox, + VBox transformationVbox, HBox startVectorField, + HBox endVectorField) { + List<String[]> transformations = getInputInformation(transformationComboBox.getValue(), + transformationVbox); + String[] startVector = getInputVector(startVectorField); + String[] endVector = getInputVector(endVectorField); controller.addCustomTransformation( - startVector, endVector, transformations, transformationName.getText()); + startVector, endVector, transformations, transformationName.getText()); } /** * Retrieves input information based on the selected transformation type. * * @param selectedTransformation the selected transformation type. - * @param transformationInputField the HBox containing the input fields for transformations. + * @param transformationVbox the HBox containing the input fields for transformations. * @return a list of Transform2D objects. */ - private List<Transform2D> getInputInformation(TransformationType selectedTransformation, - HBox transformationInputField) { + private List<String[]> getInputInformation(TransformationType selectedTransformation, + VBox transformationVbox) { if (selectedTransformation == TransformationType.JULIA) { - return getJuliaTransformation(transformationInputField); + return getJuliaTransformation(transformationVbox); } else if (selectedTransformation == TransformationType.AFFINE) { - return getAffineTransformation(transformationInputField); + return getAffineTransformation(transformationVbox); } return new ArrayList<>(); } @@ -441,18 +494,16 @@ public class MainPageView extends Scene implements ChaosGameObserver { /** * Retrieves input information for the Julia transformation. * - * @param transformationInputField the HBox containing the input fields for the Julia - * transformation. + * @param transformationVbox the HBox containing the input fields for the Julia + * transformation. * @return a list of Transform2D objects for the Julia transformation. */ - private List<Transform2D> getJuliaTransformation(HBox transformationInputField) { - List<Transform2D> list = new ArrayList<>(); - if (!transformationInputField.getChildren().isEmpty()) { - VBox juliaVbox = (VBox) transformationInputField.getChildren().get(0); - HBox juliaFields = (HBox) juliaVbox.getChildren().get(0); - list.add(new JuliaTransform( - new Complex(parseDoubleFromTextField(juliaFields, 0), - parseDoubleFromTextField(juliaFields, 1)), 1)); + private List<String[]> getJuliaTransformation(VBox transformationVbox) { + List<String[]> list = new ArrayList<>(); + if (!transformationVbox.getChildren().isEmpty()) { + HBox juliaFields = (HBox) transformationVbox.getChildren().get(0); + list.add(new String[]{((TextField) juliaFields.getChildren().get(1)).getText(), + ((TextField) juliaFields.getChildren().get(2)).getText()}); } return list; } @@ -464,23 +515,17 @@ public class MainPageView extends Scene implements ChaosGameObserver { * transformation. * @return a list of Transform2D objects for the Affine transformation. */ - private List<Transform2D> getAffineTransformation(HBox transformationInputField) { - List<Transform2D> list = new ArrayList<>(); + private List<String[]> getAffineTransformation(VBox transformationInputField) { + List<String[]> list = new ArrayList<>(); if (!transformationInputField.getChildren().isEmpty()) { - VBox affineVbox = (VBox) transformationInputField.getChildren().get(0); - for (Node node : affineVbox.getChildren().subList(1, affineVbox.getChildren().size())) { + for (Node node : transformationInputField.getChildren() + .subList(1, transformationInputField.getChildren().size())) { HBox matrixFields = (HBox) node; - - double x0 = parseDoubleFromTextField(matrixFields, 0); - double y0 = parseDoubleFromTextField(matrixFields, 1); - double x1 = parseDoubleFromTextField(matrixFields, 2); - double y1 = parseDoubleFromTextField(matrixFields, 3); - double v0 = parseDoubleFromTextField(matrixFields, 4); - double v1 = parseDoubleFromTextField(matrixFields, 5); - - Matrix2x2 matrix2x2 = new Matrix2x2(x0, y0, x1, y1); - Vector2d vector2d = new Vector2d(v0, v1); - list.add(new AffineTransform2D(matrix2x2, vector2d)); + String[] coordinateList = new String[matrixFields.getChildren().size() - 1]; + for (int i = 1; i < matrixFields.getChildren().size(); i++) { + coordinateList[i - 1] = ((TextField) matrixFields.getChildren().get(i)).getText(); + } + list.add(coordinateList); } } return list; @@ -489,23 +534,12 @@ public class MainPageView extends Scene implements ChaosGameObserver { /** * Retrieves the input vector from an HBox. * - * @param vectorHbox the HBox containing the input fields for the vector. + * @param vectorHBox the HBox containing the input fields for the vector. * @return a Vector2d object representing the input vector. */ - private Vector2d getInputVector(HBox vectorHbox) { - return new Vector2d(parseDoubleFromTextField(vectorHbox, 0), - parseDoubleFromTextField(vectorHbox, 1)); - } - - /** - * Parses a double value from the TextField at the specified index in the HBox. - * - * @param hbox the HBox containing the TextField. - * @param index the index of the TextField in the HBox. - * @return the parsed double value. - */ - private double parseDoubleFromTextField(HBox hbox, int index) { - return Double.parseDouble(((TextField) hbox.getChildren().get(index)).getText()); + private String[] getInputVector(HBox vectorHBox) { + return new String[]{((TextField) vectorHBox.getChildren().get(1)).getText(), + ((TextField) vectorHBox.getChildren().get(2)).getText()}; } /** @@ -517,15 +551,12 @@ public class MainPageView extends Scene implements ChaosGameObserver { * * @return a VBox containing the Pane and the TextFields */ - private VBox mouseBox() { - Pane box = new Pane(); - box.setPrefSize(400, 400); + private Pane mouseBox() { + StackPane box = new StackPane(); + box.setPrefWidth(125); + box.getStyleClass().add("mouse-box"); - xField = new TextField(); - yField = new TextField(); - - VBox vbox = new VBox(xField, yField); - vbox.setSpacing(10); + box.getChildren().add(new Label("Hover over me!")); box.setOnMouseMoved(e -> { double mouseX = e.getX(); @@ -535,10 +566,7 @@ public class MainPageView extends Scene implements ChaosGameObserver { updateValues(normalizedX, normalizedY); }); - VBox root = new VBox(box, vbox); - root.setSpacing(10); - - return root; + return box; } /** @@ -550,8 +578,8 @@ public class MainPageView extends Scene implements ChaosGameObserver { */ private void updateValues(double x, double y) { controller.changeJuliaTransformationDynamic(x, y); - xField.setText(String.format("X: %.1f", x)); - yField.setText(String.format("Y: %.1f", y)); + x0Field.setText(String.format(Locale.ENGLISH, "%.5f", x)); + x1Field.setText(String.format(Locale.ENGLISH, "%.5f", y)); } } diff --git a/src/main/java/edu/ntnu/idatt2003/view/ChaosImage.java b/src/main/java/edu/ntnu/idatt2003/view/components/ChaosImage.java similarity index 91% rename from src/main/java/edu/ntnu/idatt2003/view/ChaosImage.java rename to src/main/java/edu/ntnu/idatt2003/view/components/ChaosImage.java index e4d5c8880b8aeef2c41182237f8d78c6a139f37f..97d0e25f58b913f00cfc7f7fcc24494748d0fe8b 100644 --- a/src/main/java/edu/ntnu/idatt2003/view/ChaosImage.java +++ b/src/main/java/edu/ntnu/idatt2003/view/components/ChaosImage.java @@ -1,4 +1,4 @@ -package edu.ntnu.idatt2003.view; +package edu.ntnu.idatt2003.view.components; import edu.ntnu.idatt2003.model.ChaosCanvas; import javafx.beans.property.ObjectProperty; @@ -19,7 +19,7 @@ import javafx.scene.transform.Scale; * an image by a specified factor. */ -class ChaosImage { +public class ChaosImage { private static final int MAX_SCALE = 5; private static final int MIN_SCALE = 1; private static final double ZOOM_FACTOR = 1.05; @@ -51,13 +51,13 @@ class ChaosImage { } } ImageView imageView = new ImageView(image); - imageView.setFitWidth(600); - imageView.setFitHeight(600); + imageView.setFitWidth(width); + imageView.setFitHeight(height); Pane pane = new Pane(imageView); pane.setMaxHeight(height); pane.setMaxWidth(width); StackPane.setAlignment(imageView, javafx.geometry.Pos.CENTER); - pane.setClip(new Rectangle(600, 600)); + pane.setClip(new Rectangle(width, height)); enableZoom(imageView); enablePan(imageView); return pane; @@ -106,8 +106,7 @@ class ChaosImage { final double scaleBefore = imageView.getScaleX(); double scale = imageView.getScaleX() * zoomFactor; - scale = clamp(scale, MIN_SCALE, MAX_SCALE); - + scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); imageView.setScaleX(scale); imageView.setScaleY(scale); @@ -136,13 +135,13 @@ class ChaosImage { if ((newX + imageWidthHalf) / imageView.getScaleX() <= imageWidthHalf && (newX - imageWidthHalf) / imageView.getScaleX() >= -imageWidthHalf) { imageView.setTranslateX(newX); - } else if (imageView.getScaleX() <= 1) { + } else { imageView.setTranslateX(0); } if ((newY + imageHeightHalf) / imageView.getScaleY() <= imageHeightHalf && (newY - imageHeightHalf) / imageView.getScaleY() >= -imageHeightHalf) { imageView.setTranslateY(newY); - } else if (imageView.getScaleY() <= 1) { + } else { imageView.setTranslateY(0); } } @@ -196,16 +195,4 @@ class ChaosImage { double scaleBefore, double translateBefore) { return -mousePosition * scale + mousePosition * scaleBefore + translateBefore; } - - /** - * Clamp a value between a minimum and maximum value. Makes sure the value is within the bounds. - * - * @param value The value to clamp. - * @param min The minimum value. - * @param max The maximum value. - * @return The clamped value. - */ - private static double clamp(double value, double min, double max) { - return Math.max(min, Math.min(max, value)); - } } \ No newline at end of file diff --git a/src/main/java/edu/ntnu/idatt2003/view/components/StyledButton.java b/src/main/java/edu/ntnu/idatt2003/view/components/StyledButton.java new file mode 100644 index 0000000000000000000000000000000000000000..c43ed0e8cf2278f4c4efed130f1295cc1e394149 --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2003/view/components/StyledButton.java @@ -0,0 +1,52 @@ +package edu.ntnu.idatt2003.view.components; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; + +/** + * A styled button with a specified tet width, height and event handler. + * The button has a style class "button" and the event handler is set to the + * specified event handler. + * Goal: Create a styled button with a specified text, width, height and event handler. + */ +public class StyledButton extends Button { + + /** + * Creates a styled button with a specified text, width, height and event handler. + * The button has a style class "button" and the event handler is set to the + * specified event handler. + * + * @param text The text to display on the button. + * @param width The width of the button. + * @param height The height of the button. + * @param eventHandler The event handler to set on the button. + */ + public StyledButton(String text, int width, int height, + EventHandler<ActionEvent> eventHandler) { + super(text); + setPrefSize(width, height); + getStyleClass().add("button"); + setOnAction(eventHandler); + } + + /** + * Creates a styled button with a specified text, height and event handler. + * The button has a style class "button" and the event handler is set to the + * specified event handler. + * The button's width is set to grow with the HBox. + * + * @param text The text to display on the button. + * @param height The height of the button. + * @param eventHandler The event handler to set on the button. + */ + public StyledButton(String text, int height, EventHandler<ActionEvent> eventHandler) { + super(text); + HBox.setHgrow(this, Priority.ALWAYS); + setPrefHeight(height); + getStyleClass().add("button"); + setOnAction(eventHandler); + } +} diff --git a/src/main/java/edu/ntnu/idatt2003/view/components/StyledComboBox.java b/src/main/java/edu/ntnu/idatt2003/view/components/StyledComboBox.java new file mode 100644 index 0000000000000000000000000000000000000000..0f17b9ebc83e0d40c80042bf3eb45096bf57c53a --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2003/view/components/StyledComboBox.java @@ -0,0 +1,30 @@ +package edu.ntnu.idatt2003.view.components; + +import java.util.List; +import javafx.scene.control.ComboBox; + +/** + * A styled combo box with a specified prompt text, width, height and values. + * The combo box has a style class "combo-box" and the values are set to the specified values. + * Goal: Create a styled combo box with a specified prompt text, width, height and values. + */ +public class StyledComboBox<T> extends ComboBox<T> { + /** + * Creates a styled combo box with a specified prompt text, width, height and values. + * The combo box has a style class "combo-box" and the values are set to the specified values. + * + * @param promptText The text to display in the combo box when it is empty. + * @param width The preferred width of the combo box. + * @param height The preferred height of the combo box. + * @param values The values to set in the combo box. + */ + public StyledComboBox(String promptText, int width, int height, List<T> values) { + super(); + this.setPromptText(promptText); + this.setPrefSize(width, height); + this.getStyleClass().add("combo-box"); + if (values != null) { + this.getItems().addAll(values); + } + } +} diff --git a/src/main/java/edu/ntnu/idatt2003/view/components/StyledTextField.java b/src/main/java/edu/ntnu/idatt2003/view/components/StyledTextField.java new file mode 100644 index 0000000000000000000000000000000000000000..b74a2422bbfab27e9e6c63a21fa7c330bc0b304e --- /dev/null +++ b/src/main/java/edu/ntnu/idatt2003/view/components/StyledTextField.java @@ -0,0 +1,43 @@ +package edu.ntnu.idatt2003.view.components; + +import javafx.scene.control.TextField; + +import java.util.Locale; + +/** + * A styled text field with a specified prompt text, width and height. + * The text field has a style class "text-field". + * Goal: Create a styled text field with a specified prompt text, width and height. + */ +public class StyledTextField extends TextField { + /** + * Creates a styled text field with a specified prompt text, width and height. + * The text field has a style class "text-field". + * + * @param promptText The text to display in the text field when it is empty. + * @param width The preferred width of the text field. + * @param height The preferred height of the text field. + */ + public StyledTextField(String promptText, int width, int height) { + super(); + this.setPromptText(promptText); + this.setPrefSize(width, height); + this.getStyleClass().add("text-field"); + } + + /** + * Creates a styled text field with a specified coordinate, width and height. + * The coordinate is formatted to two decimal places, and is set as the text + * of the text field. + * + * @param coordinate the coordinate to display in the text field. + * @param width the preferred width of the text field. + * @param height the preferred height of the text field. + */ + public StyledTextField(double coordinate, int width, int height) { + super(String.format(Locale.US, "%.2f", coordinate)); + this.setPrefSize(width, height); + this.getStyleClass().add("text-field"); + } + +} diff --git a/src/main/resources/styles/components.css b/src/main/resources/styles/components.css new file mode 100644 index 0000000000000000000000000000000000000000..4060f2607fa1bd2872981559c86175e859675958 --- /dev/null +++ b/src/main/resources/styles/components.css @@ -0,0 +1,28 @@ +.text-field { + -fx-background-color: white; + -fx-background-radius: 0; + -fx-border-radius: 0; + -fx-font-size: 16px; +} + +.button { + -fx-background-color: #D9D9D9; + -fx-background-radius: 5px; + -fx-border-radius: 5px; + -fx-font-size: 16px; +} + +.button:hover { + -fx-background-color: #C8C8C8; +} + +.combo-box { + -fx-background-color: #D9D9D9; + -fx-background-radius: 5px; + -fx-border-radius: 5px; + -fx-font-size: 16px; +} +.combo-box:hover { + -fx-background-color: #C8C8C8; +} + diff --git a/src/main/resources/styles/mainPage.css b/src/main/resources/styles/mainPage.css index d449b672acd8f7bacb6055319b2cb3fcf81612d8..ade600d7aede831550c0ffa6c1d998be712fdefb 100644 --- a/src/main/resources/styles/mainPage.css +++ b/src/main/resources/styles/mainPage.css @@ -1,55 +1,34 @@ -.button { - -fx-background-color: #D9D9D9; - -fx-background-insets: 5px; - -fx-border-insets: 5px; - -fx-background-radius: 5px; - -fx-border-radius: 5px; - -fx-font-size: 16px; -} - -.button:hover { - -fx-background-color: #C8C8C8; -} - -.combo-box { - -fx-background-color: #D9D9D9; - -fx-background-insets: 5px; - -fx-border-insets: 5px; - -fx-background-radius: 5px; - -fx-border-radius: 5px; - -fx-font-size: 16px; -} -.combo-box:hover { - -fx-background-color: #C8C8C8; -} -.input-field { +.text-box { -fx-background-color: #D9D9D9; - -fx-background-insets: 5px; - -fx-border-insets: 5px; - -fx-background-radius: 5px; - -fx-border-radius: 5px; + -fx-background-radius: 0; + -fx-border-radius: 0; -fx-font-size: 16px; } .button-container { -fx-background-color: white; - -fx-background-radius: 10px; -fx-padding: 10px; } -.page-container { - -fx-background-color: blue; - -fx-background-radius: 10px; +.center-container { -fx-padding: 10px; } +.canvas-and-dynamic-julia-container { + -fx-padding: 20px; + -fx-border-color: #848484; + -fx-border-width: 4px; +} +.mouse-box { + -fx-background-color: #D9D9D9; +} + .main-page { -fx-background-color: #AFAFAF; -fx-border-radius: 10px; - -fx-padding: 20px; } -/* Editing Panel Styling */ + .editing-panel { -fx-background-color: white; -fx-background-radius: 10px;