diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 574de4568e21706c1ac832f7025a60df46d8ae69..7ddfc9ed476345b09db3e8fdf464a62bb2ccfa9b 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -8,6 +8,5 @@ </component> <component name="VcsDirectoryMappings"> <mapping directory="" vcs="Git" /> - <mapping directory="$PROJECT_DIR$/idatt2003" vcs="Git" /> </component> </project> \ No newline at end of file diff --git a/src/main/java/edu/ntnu/stud/chaosgame/Main.java b/src/main/java/edu/ntnu/stud/chaosgame/Main.java index 58d22a067efb0c50b4f9d1baaae4fd42e989d4b0..9b27b8b6dcdb93f6d9ddc72db03738a675ed10de 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/Main.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/Main.java @@ -1,6 +1,6 @@ package edu.ntnu.stud.chaosgame; -import edu.ntnu.stud.chaosgame.view.ChaosGameGuiView; +import edu.ntnu.stud.chaosgame.view.ChaosGameGui; import javafx.application.Application; import javafx.stage.Stage; @@ -13,7 +13,7 @@ public class Main extends Application { @Override public void start(Stage primaryStage) throws IOException { - ChaosGameGuiView view = new ChaosGameGuiView(primaryStage); + ChaosGameGui view = new ChaosGameGui(primaryStage); } diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosCanvas.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosCanvas.java index d0c91770c4d691c6ad94538084fb31363df2a035..f828143ea069922cd25511fe2b9881d31f52965a 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosCanvas.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosCanvas.java @@ -15,6 +15,12 @@ public class ChaosCanvas { */ private final int[][] canvas; + /** + * This array keeps track of how many times a pixel has been visited, + * to be used to determine the color of the pixel when displayed. + */ + private final int[][] canvasIntensityArray; + /** * Width of the canvas */ @@ -36,6 +42,9 @@ public class ChaosCanvas { */ private Vector2D maxCoords; + private int centreX; + private int centreY; + /** * Affine transformation for converting coordinates to canvas indices. */ @@ -54,8 +63,11 @@ public class ChaosCanvas { this.height = height; this.minCoords = minCoords; this.maxCoords = maxCoords; + System.out.println("Min: " + minCoords.getX0() + " " + maxCoords.getX0() + ", Max: " + maxCoords.getX0()+ " " + maxCoords.getX1()); + // Instantiate the canvas and its intensity array. this.canvas = new int[width][height]; + this.canvasIntensityArray = new int[width][height]; // Convert the coordinates to indices in the canvas this.transformCoordsToIndices = new AffineTransform2D( @@ -65,34 +77,34 @@ public class ChaosCanvas { (width-1)* minCoords.getX0() / (minCoords.getX0() - maxCoords.getX0()))); } - /** - * Get a pixel located at a point. - * - * @param point point at which the pixel is located - * @return the pixel - */ - public int getPixel(Vector2D point) { - return canvas[ (int) transformCoordsToIndices.transform(point).getX0()] - [(int) transformCoordsToIndices.transform(point).getX1()]; - } - /** * Place a pixel on the canvas. * * @param point the point where the pixel is to be placed. */ - public void putPixel(Vector2D point) { - + public void putPixel(Vector2D point, boolean countIntensity) { + if (point.getX0() > maxCoords.getX0() || point.getX0() < minCoords.getX0() || + point.getX1() > maxCoords.getX1() || point.getX1() < minCoords.getX1()) { + //PopupManager.displayError("Point out of bounds", + // "The point is out of bounds of the canvas."); + return; + } int xIndex = (int) transformCoordsToIndices.transform(point).getX0(); int yIndex = (int) transformCoordsToIndices.transform(point).getX1(); if (xIndex >= 0 && xIndex < width && yIndex >= 0 && yIndex < height) { canvas[xIndex][yIndex] = 1; + + // If the countIntensity variable is true, increment the intensity of the pixel. + if (countIntensity) { + canvasIntensityArray[xIndex][yIndex]++; + } } else { System.out.println("Index out of bounds: " + xIndex + ", " + yIndex); } } + /** * Get the width and height of the canvas in the form of an array where the first * index stores the width and the second stores the height. @@ -117,6 +129,33 @@ public class ChaosCanvas { */ public Vector2D getMaxCoords() { return this.maxCoords; } + /** + * Get a pixel located at a point. + * + * @param point point at which the pixel is located + * @return the pixel + */ + public int getPixel(Vector2D point) { + return canvas[ (int) transformCoordsToIndices.transform(point).getX0()] + [(int) transformCoordsToIndices.transform(point).getX1()]; + } + + /** + * Get the intensity of a pixel located at a point. + * + * @param point the point to check for. + * @return the intensity at the point. + */ + public int getIntensityPixel(Vector2D point) { + return canvasIntensityArray[ (int) transformCoordsToIndices.transform(point).getX0()] + [(int) transformCoordsToIndices.transform(point).getX1()]; + } + + /** + * @return the canvas intensity array. + */ + public int[][] getCanvasIntensityArray() { return this.canvasIntensityArray; } + /** * Get the number of points in the canvas with a value of 1. * @@ -178,6 +217,43 @@ public class ChaosCanvas { } } + /** + * Updates the minimum and maximum coordinates of the canvas based on the + * given center coordinates and zoom level. + * + * @param centreX the x-coordinate of the center of the subsection + * @param centreY the y-coordinate of the center of the subsection + * @param zoomLevel the zoom level + */ + public void updateCoords(double centreX, double centreY, int zoomLevel) { + // Compute the size of the subsection based on the zoom level + double size = Math.pow(4, -zoomLevel); // Adjust this formula as needed + + // Update the minimum and maximum coordinates + minCoords = new Vector2D(centreX - size / 2, centreY - size / 2); + maxCoords = new Vector2D(centreX + size / 2, centreY + size / 2); + // TODO: remove tests + System.out.println("Min: " + minCoords.getX0() + " " + maxCoords.getX0() + ", Max: " + maxCoords.getX0()+ " " + maxCoords.getX1()); + + // Update the affine transformation for converting coordinates to canvas indices + transformCoordsToIndices = new AffineTransform2D( + new Matrix2x2(0, (height-1) / (minCoords.getX1() - maxCoords.getX1()), + (width-1) / (maxCoords.getX0() - minCoords.getX0()), 0), + new Vector2D(((height-1)* maxCoords.getX1()) / (maxCoords.getX1() - minCoords.getX1()), + (width-1)* minCoords.getX0() / (minCoords.getX0() - maxCoords.getX0()))); + } + + /** + * Checks whether a given point is within the current subsection of the coordinate space. + * + * @param point the point to check + * @return true if the point is within the subsection, false otherwise + */ + public boolean isPointInCanvasRange(Vector2D point) { + return point.getX0() >= minCoords.getX0() && point.getX0() <= maxCoords.getX0() + && point.getX1() >= minCoords.getX1() && point.getX1() <= maxCoords.getX1(); + } + public int getWidth() { return width; } @@ -185,4 +261,13 @@ public class ChaosCanvas { public int getHeight() { return height; } + + /** + * Get the centre coordinates. + * + * @return the centre. + */ + public int[] getCentre() { + return new int[]{this.centreX, this.centreY}; + } } diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGame.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGame.java index cf139bf106e515321e444521a1c77235c7f57f8b..0521e77de43a953a63ee491ab5c80101574d8647 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGame.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGame.java @@ -42,6 +42,12 @@ public class ChaosGame { */ private int numOfTransforms; + /** + * Boolean informing the game whether to use colors for more frequent + * pixels or not. + */ + private boolean useColor = false; + /** * Basic parameterised constructor. * @@ -87,6 +93,22 @@ public class ChaosGame { */ public Vector2D getCurrentPoint() { return this.currentPoint; } + /** + * Get the random number generator for this chaos game. + * + * @return the RNG. + */ + public Random getRandom() { return this.random; } + + /** + * Get the description for this choas game. + * + * @return the description. + */ + public ChaosGameDescription getDescription() { + return this.description; + } + /** * Get the chaos game description. * @@ -96,6 +118,25 @@ public class ChaosGame { this.description = description; } + /** + * Set the current point from which the chaos game will proceed to iterate over + * the set of affine transforms in the chaos game description. + * + * @param point the new point. + */ + public void setCurrentPoint(Vector2D point) { + this.currentPoint = point; + } + + /** + * Set the useColor boolean. + * + * @param useColor the boolean to set. + */ + public void setUseColor(boolean useColor) { + this.useColor = useColor; + } + /** * Notify the observers that a change has occurred in the ChaosGame. @@ -128,34 +169,35 @@ public class ChaosGame { for (int i = 0; i < n; i++) { int j = this.random.nextInt( this.numOfTransforms); - switch (j) { - case 0: - results.set(0, results.get(0) + 1); - break; - case 1: - results.set(1, results.get(1) + 1); - break; - case 2: - results.set(2, results.get(2) + 1); - break; - case 3: - results.set(3, results.get(3) + 1); - break; - } + //System.out.println(j); //System.out.println(this.description.getTransforms().size()); //System.out.println("Before transform: " + currentPoint.getX0() + " " + currentPoint.getX1()); + Vector2D newPoint = this.description.getTransforms().get(j).transform(currentPoint); - - this.currentPoint = this.description.getTransforms().get(j).transform(currentPoint); - //System.out.println("After transform: " + currentPoint.getX0() + " " + currentPoint.getX1()); - this.canvas.putPixel(currentPoint); + this.currentPoint = this.findValidPoint(newPoint); + this.canvas.putPixel(currentPoint, useColor); } - //this.canvas.printCanvas(); - for (int i = 0; i < results.size(); i++) { - System.out.println("Transform " + i + " was chosen " + results.get(i) + " times."); + + } + + /** + * If the point is out of bounds, iterate until it is within bounds. Used + * if the useColor boolean is set to false. + * + * @param point the starting point. + * @return the resulting valid point within bounds. + */ + public Vector2D findValidPoint(Vector2D point) { + if (!this.canvas.isPointInCanvasRange(point) || this.canvas.getPixel(point) == 1) { + while (!this.canvas.isPointInCanvasRange(point)) { + int j = this.random.nextInt(this.numOfTransforms); + System.out.println("Before transform: " + point.getX0() + " " + point.getX1()); // Test + point = this.description.getTransforms().get(j).transform(currentPoint); + } } + return point; } } diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameFileHandler.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameFileHandler.java index 0f60eb77693e7a66a108f3c138d435b12cf7cdc6..fc93a525a4cd406a42d417b6c05d4aa94ed1868b 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameFileHandler.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameFileHandler.java @@ -42,8 +42,6 @@ public class ChaosGameFileHandler { if (firstLine.equals("Affine2D")) { - - // Read the minimum vector if (scanner.hasNextDouble()) { double x0min = scanner.nextDouble(); diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameTask.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameTask.java new file mode 100644 index 0000000000000000000000000000000000000000..d745f3c814ac9b56968603870c988e78c09fe039 --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/ChaosGameTask.java @@ -0,0 +1,56 @@ +package edu.ntnu.stud.chaosgame.controller.game; +import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; +import edu.ntnu.stud.chaosgame.model.data.Vector2D; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; + +// Step 1: Create a Callable tasks +class ChaosGameTask implements Callable<Void> { + private final int start; + private final int end; + private final ChaosGame game; + + public ChaosGameTask(ChaosGame game, int start, int end) { + this.game = game; + this.start = start; + this.end = end; + } + + @Override + public Void call() { + for (int i = start; i < end; i++) { + int j = game.getRandom().nextInt(game.getDescription().getTransforms().size()); + Vector2D newPoint = game.getDescription().getTransforms().get(j).transform(game.getCurrentPoint()); + game.setCurrentPoint(game.findValidPoint(newPoint)); + game.getCanvas().putPixel(game.getCurrentPoint(), false); + } + return null; + } + + // Step 2, 3, 4: Divide the task into smaller tasks and execute them in parallel + public void runSteps(int n) { + int numProcessors = Runtime.getRuntime().availableProcessors(); + ExecutorService executor = Executors.newFixedThreadPool(numProcessors); + + int chunkSize = n / numProcessors; + List<Future<Void>> futures = new ArrayList<>(); + + for (int i = 0; i < numProcessors; i++) { + int start = i * chunkSize; + int end = (i == numProcessors - 1) ? n : start + chunkSize; // handle remainder + Callable<Void> task = new ChaosGameTask(this.game, start, end); + futures.add(executor.submit(task)); + } + + for (Future<Void> future : futures) { + try { + future.get(); // wait for task to complete + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + executor.shutdown(); // always remember to shutdown the executor + } +} \ No newline at end of file diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/Quadtree.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/Quadtree.java new file mode 100644 index 0000000000000000000000000000000000000000..32915e43675e4d141cd806ceb5f0f8e4eb3e7a71 --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/Quadtree.java @@ -0,0 +1,34 @@ +package edu.ntnu.stud.chaosgame.controller.game; + +import javafx.geometry.Point2D; +import javafx.scene.shape.Rectangle; + +public class Quadtree { + private QuadtreeNode root; + + public Quadtree(Rectangle bounds) { + root = new QuadtreeNode(bounds, 0); + } + + public QuadtreeNode findNodeContainingPoint(Point2D point) { + return findNodeContainingPoint(root, point); + } + + private QuadtreeNode findNodeContainingPoint(QuadtreeNode node, Point2D point) { + if (node.isLeaf()) { + return node; + } + + for (QuadtreeNode child : node.getChildren()) { + if (child.getBounds().contains(point)) { + return findNodeContainingPoint(child, point); + } + } + + return null; + } + + + + // other methods omitted for brevity +} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/game/QuadtreeNode.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/QuadtreeNode.java new file mode 100644 index 0000000000000000000000000000000000000000..ea7b7940e476da2c71ee83c7182f1bcbba1d2afa --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/game/QuadtreeNode.java @@ -0,0 +1,111 @@ +package edu.ntnu.stud.chaosgame.controller.game; + +import javafx.scene.shape.Rectangle; + +public class QuadtreeNode { + private QuadtreeNode parent; + private QuadtreeNode[] children; + + /** + * Boolean representing whether the node is a leaf node or + * a child node. + */ + private boolean isLeaf; + + /** + * Integer describing which recursion level this node belongs to. + */ + private int recursionLevel; + private Rectangle bounds; + + private double minX; + private double minY; + private double maxX; + private double maxY; + + + public QuadtreeNode(Rectangle bounds, int recursionLevel) { + this.bounds = bounds; + this.minX = bounds.getX(); + this.minY = bounds.getY() - bounds.getHeight(); + this.maxX = bounds.getX() + bounds.getWidth(); + this.maxY = bounds.getY(); + + this.recursionLevel = recursionLevel; + this.isLeaf = true; + this.children = new QuadtreeNode[4]; + } + + /** + * Split this parent node into four child nodes. + */ + public void split() { + if (!isLeaf) { + return; + } + + double halfWidth = bounds.getWidth() / 2; + double halfHeight = bounds.getHeight() / 2; + + children[0] = new QuadtreeNode(new Rectangle(bounds.getX(), bounds.getY(), halfWidth, halfHeight), recursionLevel + + 1); + children[1] = new QuadtreeNode(new Rectangle(bounds.getX() + halfWidth, bounds.getY(), halfWidth, halfHeight), recursionLevel + + 1); + children[2] = new QuadtreeNode(new Rectangle(bounds.getX(), bounds.getY() + halfHeight, halfWidth, halfHeight), recursionLevel + + 1); + children[3] = new QuadtreeNode(new Rectangle(bounds.getX() + halfWidth, bounds.getY() + halfHeight, halfWidth, halfHeight), recursionLevel + + 1); + + isLeaf = false; + } + + /** + * Merge this node with its three counterparts to form a single child node. + */ + public void merge() { + if (isLeaf) { + return; + } + + for (int i = 0; i < 4; i++) { + children[i] = null; + } + + isLeaf = true; + } + + /** + * Check if this node contains or is adjacent to a specified point in the plane. + * + * @param centerX the centre of the point in the x-axis. + * @param centerY the centre of the point in the y-axis. + * @return a boolean representing the result o the check. + */ + public boolean containsOrAdjacent(double centerX, double centerY) { + // Check if the point is within the node + if (centerX >= this.minX && centerX <= this.maxX && centerY >= this.minY && centerY <= this.maxY) { + return true; + } + + // Check if the point is adjacent to the node + if (centerX >= this.minX - 1 && centerX <= this.maxX + 1 && centerY >= this.minY - 1 && centerY <= this.maxY + 1) { + return true; + } + + return false; + } + + public Rectangle getBounds() { + return this.bounds; + } + + public QuadtreeNode[] getChildren() { + return this.children; + } + + public Boolean isLeaf() { + return this.isLeaf; + } + + // getters and setters omitted for brevity +} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/utility/Formatter.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/utility/Formatter.java new file mode 100644 index 0000000000000000000000000000000000000000..aa4045a091c44980b024615a170ed66b5292a535 --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/utility/Formatter.java @@ -0,0 +1,53 @@ +package edu.ntnu.stud.chaosgame.controller.utility; + +import java.util.function.UnaryOperator; +import javafx.scene.control.TextFormatter; +import javafx.scene.control.TextFormatter.Change; + +/** + * This class sets formatting constraints for certain UI components. + */ +public class Formatter { + + + /** + * A formatter for text fields that only allows for floating point numbers. + */ + public static UnaryOperator<Change> floatFormatter = change -> { + String newText = change.getControlNewText(); + if (newText.matches("([0-9]*[.])?[0-9]*")) { + return change; + } + return null; + }; + + /** + * A formatter for text fields that only allows for integers. + */ + public static UnaryOperator<TextFormatter.Change> integerFormatter = change -> { + String newText = change.getControlNewText(); + if (newText.matches("[0-9]*")) { + return change; + } + return null; +}; + + + /** + * Get the float formatter as a TextFormatter. + * + * @return the float formatter. + */ + public static TextFormatter<Change> getFloatFormatter() { + return new TextFormatter<>(floatFormatter); + } + + /** + * Get the integer formatter as a TextFormatter. + * + * @return the integer formatter. + */ + public static TextFormatter<Change> getIntFormatter() { + return new TextFormatter<>(integerFormatter); + } +} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosCanvasToImageConverter.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosCanvasToImageConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..074dcc7ca1ff4b2ecf80d97cb5ca287988dbcf29 --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosCanvasToImageConverter.java @@ -0,0 +1,102 @@ +package edu.ntnu.stud.chaosgame.view; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.paint.Color; +import edu.ntnu.stud.chaosgame.controller.game.ChaosCanvas; + +/** + * This class converts the state of a ChaosCanvas to a WritableImage. + */ +public class ChaosCanvasToImageConverter { + + /** + * The image to be created. + */ + private WritableImage image; + + /** + * Convert the canvas to a writable image. + * + * @param chaosCanvas the canvas to work upon. + */ + public ChaosCanvasToImageConverter(ChaosCanvas chaosCanvas, boolean useIntensity) { + if (!useIntensity) { + this.convertWithoutIntensity(chaosCanvas); + } else { + this.convertWithIntensity(chaosCanvas); + } + } + + + /** + * Get the image. + * + * @return the image. + */ + public WritableImage getImage() { + return image; + } + + /** + * Convert the ChaosCanvas to an image using the intensity of the pixels + * to determine the color. + * + * @param canvas the ChaosCanvas to convert. + */ + public void convertWithIntensity(ChaosCanvas canvas) { + int width = canvas.getWidth(); + int height = canvas.getHeight(); + image = new WritableImage(width, height); + PixelWriter pixelWriter = image.getPixelWriter(); + + int[][] canvasArray = canvas.getCanvasArray(); + int[][] canvasIntensityArray = canvas.getCanvasIntensityArray(); + int maxIntensity = 0; + + // Find the maximum intensity. + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + if (canvasArray[i][j] == 1 && canvasIntensityArray[i][j] > maxIntensity) { + maxIntensity = canvasIntensityArray[i][j]; + } + } + } + + // Map the intensity to a hue value. + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + if (canvasArray[i][j] == 1) { + int intensity = canvasIntensityArray[i][j]; + double hue = (double) intensity / maxIntensity * 360; // Map intensity to hue + pixelWriter.setColor(j, i, Color.hsb(hue, 1.0, 1.0)); // Full saturation and brightness + } else { + pixelWriter.setColor(j, i, Color.WHITE); + } + } + } + } + + /** + * Convert the ChaosCanvas to an image without concern for the intensity of the pixels. + * + * @param chaosCanvas the ChaosCanvas to convert. + */ + public void convertWithoutIntensity(ChaosCanvas chaosCanvas) { + int width = chaosCanvas.getWidth(); + int height = chaosCanvas.getHeight(); + image = new WritableImage(width, height); + PixelWriter pixelWriter = image.getPixelWriter(); + + int[][] canvasArray = chaosCanvas.getCanvasArray(); + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + if (canvasArray[i][j] == 1) { + pixelWriter.setColor(j, i, Color.BLACK); + } else { + pixelWriter.setColor(j, i, Color.WHITE); + } + } + } + } + +} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGui.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGui.java new file mode 100644 index 0000000000000000000000000000000000000000..baea5bfd55c60d9ef4debd535693bfe61392abfe --- /dev/null +++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGui.java @@ -0,0 +1,495 @@ +package edu.ntnu.stud.chaosgame.view; + +import edu.ntnu.stud.chaosgame.controller.game.ChaosCanvas; +import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; +import edu.ntnu.stud.chaosgame.controller.game.ChaosGameDescription; +import edu.ntnu.stud.chaosgame.controller.utility.Formatter; +import edu.ntnu.stud.chaosgame.model.data.Vector2D; +import edu.ntnu.stud.chaosgame.model.generators.ChaosGameDescriptionFactory; + +import java.util.Objects; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.animation.TranslateTransition; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import javafx.util.Duration; + +import java.io.IOException; + +public class ChaosGameGui implements ChaosGameObserver { + private int currentLine = 0; + + + private Canvas canvas; + + /** + * The ChaosCanvas for this GUI. + */ + private ChaosCanvas chaosCanvas; + + /** + * The ChaosGameDescription. + */ + private ChaosGameDescription description; + + /** + * The ChaosGameDescriptionFactory. + */ + private ChaosGameDescriptionFactory factory; + + /** + * The ImageView for the GUI.. + */ + private ChaosGameImageView imageView; + + /** + * The Scene for the GUI.. + */ + private Scene scene; + + /** + * The width and height of the GUI. + */ + private int width; + private int height; + + /** + * The ChaosGame for this GUI. + */ + private ChaosGame game; + + /** + * The Timeline for the GUI. + */ + private Timeline timeline; + + /** + * The BorderPane for the GUI. + */ + private BorderPane borderPane; + + /** + * The side menu for the GUI. + */ + private VBox sideMenu; + + /** + * The start, stop, new, clear, quit and show sidebar buttons for the GUI. + */ + private Button startButton; + private Button stopButton; + private Button newButton; + private Button clearButton; + private Button quitButton; + private Button sideMenuButton; + + /** + * The load fractal from file and write fractal to file buttons for the GUI. + */ + private Button loadFractalFromFileButton; + private Button writeFractalToFileButton; + + /** + * The radio buttons for the fractal type for the GUI. + */ + private RadioButton sierpinskiRadioButton; + private RadioButton barnsleyRadioButton; + private RadioButton juliaRadioButton; + private RadioButton improvedBarnsleyButton; + + private TextField stepCountTextField; + + private CheckBox colorCheckBox; + + + public ChaosGameGui(Stage primaryStage) throws IOException { + + this.initializeComponents(); + + primaryStage.setTitle("Fractal Chaos Game"); + primaryStage.setScene(scene); + primaryStage.setOnShown(event -> this.imageView.requestFocus()); + primaryStage.show(); + + } + + /** + * Initialize the components of the GUI. + */ + private void initializeComponents() { + + // Timeline + this.timeline = new Timeline(new KeyFrame(Duration.seconds(0.05), event -> this.drawChaosGame())); + this.initializeImageView(); + + // Side menu + + //TEMPORARY CODE to test Chaos Games in GUI + this.initializeGameComponents(); + + this.initializeMainButtons(); + this.initializeFractalButtons(); + this.initializeSideMenu(); + + this.scene = new Scene(this.borderPane,1700,1000); + } + + /** + * Initialize the main buttons for the GUI. + */ + private void initializeMainButtons() { + this.startButton = new Button("Start"); + startButton.setOnAction(event -> timeline.play()); + this.stopButton = new Button("Stop"); + stopButton.setOnAction(event -> timeline.stop()); + + this.newButton = new Button("New"); + + newButton.setOnAction(event ->{ + this.canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + chaosCanvas.clearCanvas(); + }); + + this.clearButton = new Button("Clear"); + + clearButton.setOnAction(event -> { + getImageView().setImage(null); + setCurrentLine(0); + }); + + // Quit button + this.quitButton = new Button("Quit"); + quitButton.setOnAction(event -> Platform.exit()); + + } + + /** + * Initialize the components related to the chaos game itself. + */ + private void initializeGameComponents() { + // Description + this.factory = new ChaosGameDescriptionFactory(); + this.description = factory.getDescriptions().get(0); + + this.chaosCanvas = new ChaosCanvas(100, 100, this.description.getMinCoords(), + this.description.getMaxCoords()); + game = new ChaosGame(this.description, chaosCanvas); + } + + /** + * Initialize components related to the image view and zoom function. + */ + private void initializeImageView() { + // Image view + this.imageView = new ChaosGameImageView(this); + width = 1000; + height = 1000; + this.canvas = new Canvas(width, height); + //this.imageView.setImage(canvas.snapshot(null, null)); + + this.clearImageView(); + + } + + /** + * Color the entire image view white. + */ + private void clearImageView() { + GraphicsContext gc = canvas.getGraphicsContext2D(); + gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight()); + imageView.setImage(null); + } + + /** + * Initialize the buttons related to managing the fractals. + */ + private void initializeFractalButtons() { + + // Radio buttons for choosing fractal type + ToggleGroup group = new ToggleGroup(); + this.sierpinskiRadioButton = new RadioButton("Sierpinski"); + sierpinskiRadioButton.setToggleGroup(group); + sierpinskiRadioButton.setSelected(true); + this.barnsleyRadioButton = new RadioButton("Barnsley"); + barnsleyRadioButton.setToggleGroup(group); + this.juliaRadioButton = new RadioButton("Julia"); + juliaRadioButton.setToggleGroup(group); + this.improvedBarnsleyButton = new RadioButton("Improved Barnsley"); + improvedBarnsleyButton.setToggleGroup(group); + + + // Set action for Sierpinski radio button. + sierpinskiRadioButton.setOnAction(event -> { + this.updateDescription(0); + }); + + // Set action for Barnsley radio button. + barnsleyRadioButton.setOnAction(event -> { + this.updateDescription(1); + }); + + // Set action for Julia radio button. + juliaRadioButton.setOnAction(event -> { + this.updateDescription(2); + }); + + improvedBarnsleyButton.setOnAction(event -> { + this.updateDescription(3); + }); + + // Load fractal file button + this.loadFractalFromFileButton = new Button("Load Fractal"); + // Write fractal to file button + this.writeFractalToFileButton = new Button("Write to File"); + } + + /** + * Initialize the side menu. + */ + private void initializeSideMenu() { + this.sideMenu = new VBox(); + // Parameters + VBox parameterBox = new VBox(); + + // Step Count GUI + VBox stepCountBox = new VBox(); + Label stepCountLabel = new Label("Step Count"); + this.stepCountTextField = new TextField(); + this.stepCountTextField.setTextFormatter(Formatter.getIntFormatter()); // Set formatter + stepCountTextField.setPrefHeight(5); + stepCountTextField.setPrefWidth(50); + + stepCountBox.getChildren().addAll(stepCountLabel,stepCountTextField); + + // Minimum Coordinates GUI + VBox minCoordinatesBox = new VBox(); + Label minCoordinatesLabel = new Label("Min. Coordinates"); + TextField minimumCoordinatesTextField = new TextField(); + minimumCoordinatesTextField.setPrefHeight(5); + minimumCoordinatesTextField.setPrefWidth(50); + Button changeMinimumCoordinatesButton = new Button("Change Min. Coordinates"); + minCoordinatesBox.getChildren().addAll(minCoordinatesLabel, + minimumCoordinatesTextField,changeMinimumCoordinatesButton); + + // Maximum Coordinates GUI + VBox maxCoordinatesBox = new VBox(); + Label maxCoordinatesLabel = new Label("Max Coordinates"); + TextField maximumCoordinatesTextField = new TextField(); + maximumCoordinatesTextField.setPrefHeight(5); + maximumCoordinatesTextField.setPrefWidth(50); + Button changeMaximumCoordinatesButton = new Button("Change Max Coordinates"); + maxCoordinatesBox.getChildren().addAll(maxCoordinatesLabel, + maximumCoordinatesTextField,changeMaximumCoordinatesButton); + + HBox colorBox = new HBox(); + Label colorLabel = new Label("Use color"); + this.colorCheckBox = new CheckBox(); + Region colorRegion = new Region(); + colorRegion.setMinWidth(30); + colorBox.getChildren().addAll(colorCheckBox, colorRegion, colorLabel); + + Region separator1 = new Region(); + separator1.setMinHeight(10); + Region separator2 = new Region(); + separator2.setMinHeight(10); + + // Fill parameter box + parameterBox.getChildren().addAll(stepCountBox, minCoordinatesBox, maxCoordinatesBox); + parameterBox.setPadding(new Insets(10)); + + // Add basic control buttons + sideMenu.getChildren().addAll(startButton,stopButton,newButton,clearButton); + + // Add fractal radio buttons + sideMenu.getChildren().addAll(sierpinskiRadioButton, barnsleyRadioButton, juliaRadioButton, + improvedBarnsleyButton); + + sideMenu.getChildren().addAll(separator1, colorBox, separator2); + this.initializeColorButtonHandler(); + + // Add parameter VBox + sideMenu.getChildren().add(parameterBox); + + // Add file buttons + sideMenu.getChildren().addAll(loadFractalFromFileButton,writeFractalToFileButton); + + // Add quit button + sideMenu.getChildren().add(quitButton); + + // Add padding + sideMenu.setPadding(new Insets(10)); + + // Create split pane and button to toggle sidebar + this.sideMenuButton = new Button(">>"); + this.initializeSideButtonHandler(); + Region sideMenuButtonRegion = new Region(); + sideMenuButtonRegion.setMinWidth(200); + HBox sideMenuButtonBox = new HBox(); + sideMenuButtonBox.getChildren().addAll(sideMenuButtonRegion, sideMenuButton); + + // The right VBox containing both the sidebar and the sidebar toggle button. + VBox rightVBox = new VBox(); + + rightVBox.getChildren().addAll(sideMenuButtonBox, sideMenu); + this.sideMenu.setStyle("-fx-background-color: lightgrey; -fx-background-radius: 3;"); + + this.borderPane = new BorderPane(); + this.borderPane.setCenter(imageView); + this.borderPane.setRight(rightVBox); + imageView.setFocusTraversable(true); + rightVBox.setFocusTraversable(false); + borderPane.setFocusTraversable(false); + + } + + /** + * Initialise the side bar button handler, allowing the user + * to show or hide the right sidebar. + */ + private void initializeSideButtonHandler() { + TranslateTransition openNav = new TranslateTransition(new Duration(350), sideMenu); + openNav.setToX(0); + TranslateTransition closeNav = new TranslateTransition(new Duration(350), sideMenu); + + this.sideMenuButton.setOnAction(e -> { + if(sideMenu.getTranslateX() != 0){ + this.sideMenuButton.setText(">>"); + openNav.play(); + } else { + closeNav.setToX(sideMenu.getWidth()); + closeNav.play(); + this.sideMenuButton.setText("<<"); + } + }); + } + + /** + * Initialize the color button handler. + */ + private void initializeColorButtonHandler() { + this.colorCheckBox.setOnAction(event -> { + game.setUseColor(colorCheckBox.isSelected()); + this.clearImageView(); + this.chaosCanvas.clearCanvas(); + }); + } + + /** + * Get the chaos canvas of this GUI view. + * + * @return the canvas. + */ + public ChaosCanvas getChaosCanvas() { + return this.chaosCanvas; + } + + public void drawChaosGame(){ + // Run the number of steps specified in text field, else 1000. + game.runSteps(!Objects.equals(this.stepCountTextField.getText(), "") ? + Integer.parseInt(this.stepCountTextField.getText()) : 1000); + + // Convert the canvas to either an image with coloured pixels based on intensity, or just black and white. + ChaosCanvasToImageConverter converter = new ChaosCanvasToImageConverter(this.chaosCanvas, + this.colorCheckBox.isSelected()); + WritableImage image = converter.getImage(); + this.canvas.getGraphicsContext2D().drawImage(image, 0, 0); + this.imageView.setImage(image); + } + + public int getWidth(){ + return this.width; + } + public int getHeight(){ + return this.height; + } + public ImageView getImageView(){ + return this.imageView; + } + + public void setCurrentLine(int currentLine) { + this.currentLine = currentLine; + } + + public void setImageViewFromImage(Image inputView) { + this.imageView.setImage(inputView); + } + + /** + * Update the description of the chaos game. + * TODO: this method may need to be changed depending on how we implement the UI. Rename to update? + * + * @param index the index of the new description in the list of factory descriptions. + * + */ + @Override + public void updateDescription(int index) { + timeline.stop(); + this.chaosCanvas.clearCanvas(); + this.canvas.getGraphicsContext2D().clearRect(0, 0, this.canvas.getGraphicsContext2D(). + getCanvas().getWidth(), this.canvas.getGraphicsContext2D().getCanvas().getHeight()); + this.clearImageView(); + this.chaosCanvas.clearCanvas(); + + this.description = this.factory.getDescriptions().get(index); // Assuming the Sierpinski description is at index 0 + this.chaosCanvas = new ChaosCanvas(1000, 1000, this.description.getMinCoords(), + this.description.getMaxCoords()); + this.updateCanvas(this.chaosCanvas); + game = new ChaosGame(this.description, this.chaosCanvas); + game.setUseColor(this.colorCheckBox.isSelected()); + //this.game.setDescription(description); + } + + /** + * Update the canvas and set a new zoom factor for the image view based on the ratio + * between the old and new canvas heights. + * + * @param canvas the canvas to update with. + */ + @Override + public void updateCanvas(ChaosCanvas canvas) { + float zoomRatio = (float) this.chaosCanvas.getHeight() / canvas.getHeight(); + //this.imageView.fixedZoom(zoomRatio); // Set new zoom factor. + this.chaosCanvas = canvas; + } + + /** + * Update which parts of the fractal are rendered and at what level of detail. + * + * @param zoomLevel the number of recursive zoom levels. + * @param centreX the x-coordinate of the centre of the image view. + * @param centreY the y-coordinate of the centre of the image view. + */ + public void updateDetail(int zoomLevel, double centreX, double centreY) { + this.clearImageView(); + this.chaosCanvas.clearCanvas(); + this.chaosCanvas.updateCoords(centreX, centreY, zoomLevel); + this.game.setCurrentPoint(new Vector2D(centreX, centreY)); + } + + /** + * Update the observer based on changes to the chaos game. + * TODO: this method may need to be changed depending on how we implement the UI. The update method may need to be split. + * + * @param game the game this observer is monitoring. + */ + @Override + public void update(ChaosGame game) { + //drawChaosGame(); + } + + +} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java deleted file mode 100644 index 8221f304e7a35397b695cba8993dbda578a92c35..0000000000000000000000000000000000000000 --- a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java +++ /dev/null @@ -1,468 +0,0 @@ -package edu.ntnu.stud.chaosgame.view; - -import edu.ntnu.stud.chaosgame.controller.game.ChaosCanvas; -import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; -import edu.ntnu.stud.chaosgame.controller.game.ChaosGameDescription; -import edu.ntnu.stud.chaosgame.model.generators.ChaosGameDescriptionFactory; - -import edu.ntnu.stud.chaosgame.model.transformations.AffineTransform2D; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.geometry.Insets; -import javafx.scene.Scene; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.image.PixelWriter; -import javafx.scene.image.WritableImage; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import javafx.stage.Stage; -import javafx.util.Duration; - -import java.io.IOException; -import java.util.concurrent.atomic.AtomicReference; - -public class ChaosGameGuiView implements ChaosGameObserver { - private int currentLine = 0; - - /** - * The ChaosCanvas for this GUI. - */ - private ChaosCanvas canvas; - - /** - * The ChaosGameDescription. - */ - private ChaosGameDescription description; - - /** - * The ChaosGameDescriptionFactory. - */ - private ChaosGameDescriptionFactory factory; - - /** - * The AtomicReference for the ChaosGameDescription. - */ - private AtomicReference<ChaosGameDescription> descriptionRef; - - /** - * The PixelWriter for the GUI. - */ - private PixelWriter pixelWriter; - - /** - * The ImageView for the GUI. - */ - private ChaosGameImageView imageView; - - /** - * The Scene for the GUI. - */ - private Scene scene; - - /** - * The width and height of the GUI. - */ - private int width; - private int height; - - /** - * The ChaosGame for this GUI. - */ - private ChaosGame game; - - /** - * The Timeline for the GUI. - */ - private Timeline timeline; - - /** - * The BorderPane for the GUI. - */ - private BorderPane borderPane; - - /** - * The side menu for the GUI. - */ - private VBox sideMenu; - - /** - * The start, stop, new, clear, and quit buttons for the GUI. - */ - private Button startButton; - private Button stopButton; - private Button newButton; - private Button clearButton; - private Button quitButton; - - /** - * The load fractal from file and write fractal to file buttons for the GUI. - */ - private Button loadFractalFromFileButton; - private Button writeFractalToFileButton; - - /** - * The radio buttons for the fractal type for the GUI. - */ - private RadioButton sierpinskiRadioButton; - private RadioButton barnsleyRadioButton; - private RadioButton juliaRadioButton; - private RadioButton improvedBarnsleyButton; - - private final int resolutionHeight = 1000; - private final int resolutionWidth = 1000; - - - public ChaosGameGuiView(Stage primaryStage) throws IOException { - - this.initializeComponents(); - - primaryStage.setTitle("Fractal Chaos Game"); - primaryStage.setScene(scene); - primaryStage.setOnShown(event -> this.imageView.requestFocus()); - primaryStage.show(); - - } - - /** - * Initialize the components of the GUI. - */ - private void initializeComponents() { - - // Timeline - this.timeline = new Timeline(new KeyFrame(Duration.seconds(0.05), event -> this.drawChaosGame())); - this.initializeImageView(); - this.timeline.setCycleCount(Timeline.INDEFINITE); - - - // Side menu - - //TEMPORARY CODE to test Chaos Games in GUI - this.initializeGameComponents(); - - this.initializeMainButtons(); - this.initializeFractalButtons(); - this.initializeSideMenu(); - - this.scene = new Scene(this.borderPane,1700,1000); - } - - /** - * Initialize the main buttons for the GUI. - */ - private void initializeMainButtons() { - this.startButton = new Button("Start"); - startButton.setOnAction(event -> timeline.play()); - this.stopButton = new Button("Stop"); - stopButton.setOnAction(event -> timeline.stop()); - - this.newButton = new Button("New"); - - newButton.setOnAction(event ->{ - WritableImage newWritableImage = new WritableImage(width, height); - setPixelWriter(newWritableImage.getPixelWriter()); - setImageViewFromImage(newWritableImage); - canvas.clearCanvas(); - }); - - this.clearButton = new Button("Clear"); - - clearButton.setOnAction(event -> { - getImageView().setImage(null); - setCurrentLine(0); - }); - - // Quit button - this.quitButton = new Button("Quit"); - quitButton.setOnAction(event -> Platform.exit()); - - } - - /** - * Initialize the components related to the chaos game itself. - */ - private void initializeGameComponents() { - // Description - this.factory = new ChaosGameDescriptionFactory(); - this.descriptionRef = new AtomicReference<>(factory.getDescriptions().get(0)); - this.description = descriptionRef.get(); - - this.canvas = new ChaosCanvas(resolutionWidth, resolutionHeight, descriptionRef.get().getMinCoords(), descriptionRef.get().getMaxCoords()); - game = new ChaosGame(this.description, canvas); - } - - /** - * Initialize components related to the image view and zoom function. - */ - private void initializeImageView() { - // Image view - this.imageView = new ChaosGameImageView(this); - width = resolutionWidth; - height = resolutionHeight; - WritableImage writableImage = new WritableImage(width, height); - pixelWriter = writableImage.getPixelWriter(); - this.imageView.setImage(writableImage); - this.imageView.setFitHeight(1000); - this.imageView.setFitWidth(1000); - - this.fillImageView(); - /*imageView.setOnScroll(event -> { - double zoomFactor = 1.05; // This is the zoom factor per scroll - double movement = event.getDeltaY(); - - imageView.setTranslateX(imageView.getTranslateX() - event.getDeltaX()); - imageView.setTranslateY(imageView.getTranslateY() - event.getDeltaY()); - - if (movement > 0) { - imageView.setScaleX(imageView.getScaleX() * zoomFactor); - imageView.setScaleY(imageView.getScaleY() * zoomFactor); - } else { - imageView.setScaleX(imageView.getScaleX() / zoomFactor); - imageView.setScaleY(imageView.getScaleY() / zoomFactor); - } - - // Limit the zoom so the scale doesn't become too big or too small - imageView.setScaleX(Math.max(imageView.getScaleX(), 1.0)); - imageView.setScaleY(Math.max(imageView.getScaleY(), 1.0)); - imageView.setScaleX(Math.min(imageView.getScaleX(), 10.0)); // max scale - imageView.setScaleY(Math.min(imageView.getScaleY(), 10.0)); // max scale - - event.consume(); // consume the event so it doesn't propagate further - });*/ - - } - - /** - * Color the entire image view white to facilitate scrolling. - */ - private void fillImageView() { - // Color the image white - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - pixelWriter.setColor(x, y, Color.WHITE); - } - } - - } - - /** - * Initialize the buttons related to managing the fractals. - */ - private void initializeFractalButtons() { - - // Radio buttons for choosing fractal type - ToggleGroup group = new ToggleGroup(); - this.sierpinskiRadioButton = new RadioButton("Sierpinski"); - sierpinskiRadioButton.setToggleGroup(group); - sierpinskiRadioButton.setSelected(true); - this.barnsleyRadioButton = new RadioButton("Barnsley"); - barnsleyRadioButton.setToggleGroup(group); - this.juliaRadioButton = new RadioButton("Julia"); - juliaRadioButton.setToggleGroup(group); - this.improvedBarnsleyButton = new RadioButton("Improved Barnsley"); - improvedBarnsleyButton.setToggleGroup(group); - - - AtomicReference<ChaosCanvas> canvasRef = new AtomicReference<>(canvas); - - // Set action for Sierpinski radio button. - sierpinskiRadioButton.setOnAction(event -> { - timeline.stop(); - //canvasRef.get().clearCanvas(); - WritableImage newWritableImage = new WritableImage(width, height); - setPixelWriter(newWritableImage.getPixelWriter()); - setImageViewFromImage(newWritableImage); - this.fillImageView(); - //canvas.clearCanvas(); - - this.descriptionRef.set(factory.getDescriptions().get(0)); // Assuming the Sierpinski description is at index 0 - canvasRef.set(new ChaosCanvas(resolutionWidth, resolutionHeight, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords())); - game = new ChaosGame(this.descriptionRef.get(), canvasRef.get()); - }); - - // Set action for Barnsley radio button. - barnsleyRadioButton.setOnAction(event -> { - timeline.stop(); - //canvasRef.get().clearCanvas(); - WritableImage newWritableImage = new WritableImage(width, height); - setPixelWriter(newWritableImage.getPixelWriter()); - setImageViewFromImage(newWritableImage); - this.fillImageView(); - //canvas.clearCanvas(); - this.descriptionRef.set(factory.getDescriptions().get(1)); // Assuming the Sierpinski description is at index 0 - canvasRef.set(new ChaosCanvas(resolutionWidth, resolutionHeight, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords())); - game = new ChaosGame(this.descriptionRef.get(), canvasRef.get()); - }); - - // Set action for Julia radio button. - juliaRadioButton.setOnAction(event -> { - timeline.stop(); - //canvasRef.get().clearCanvas(); - WritableImage newWritableImage = new WritableImage(width, height); - setPixelWriter(newWritableImage.getPixelWriter()); - setImageViewFromImage(newWritableImage); - this.fillImageView(); - //canvas.clearCanvas(); - this.descriptionRef.set(factory.getDescriptions().get(2)); // Assuming the Sierpinski description is at index 0 - canvasRef.set(new ChaosCanvas(resolutionWidth, resolutionHeight, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords())); - game = new ChaosGame(this.descriptionRef.get(), canvasRef.get()); - }); - - improvedBarnsleyButton.setOnAction(event -> { - timeline.stop(); - //canvasRef.get().clearCanvas(); - WritableImage newWritableImage = new WritableImage(width, height); - setPixelWriter(newWritableImage.getPixelWriter()); - setImageViewFromImage(newWritableImage); - this.fillImageView(); - //canvas.clearCanvas(); - - // Test - this.descriptionRef.set(factory.getDescriptions().get(3)); // Assuming the Sierpinski description is at index 0 - this.description = factory.getDescriptions().get(3); - - for (int i = 0; i < this.description.getTransforms().size(); i++) { - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getMatrix().getA00()); - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getMatrix().getA01()); - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getMatrix().getA10()); - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getMatrix().getA11()); - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getVector().getX0()); - System.out.println(((AffineTransform2D) this.description.getTransforms().get(i)).getVector().getX1()); - } - - - canvasRef.set(new ChaosCanvas(resolutionWidth, resolutionHeight, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords())); - game = new ChaosGame(this.descriptionRef.get(), canvasRef.get()); - }); - - // Load fractal file button - this.loadFractalFromFileButton = new Button("Load Fractal"); - // Write fractal to file button - this.writeFractalToFileButton = new Button("Write to File"); - } - - /** - * Initialize the side menu. - */ - private void initializeSideMenu() { - this.sideMenu = new VBox(); - // Parameters - VBox parameterBox = new VBox(); - // Step Count GUI - VBox stepCountBox = new VBox(); - Label stepCountLabel = new Label("Step Count"); - TextArea stepCountTextArea = new TextArea(); - stepCountTextArea.setPrefHeight(5); - stepCountTextArea.setPrefWidth(50); - Button changeStepCountButton = new Button("Change Step Count"); - stepCountBox.getChildren().addAll(stepCountLabel,stepCountTextArea, changeStepCountButton); - // Minimum Coordinates GUI - VBox minCoordinatesBox = new VBox(); - Label minCoordinatesLabel = new Label("Min. Coordinates"); - TextArea minimumCoordinatesTextArea = new TextArea(); - minimumCoordinatesTextArea.setPrefHeight(5); - minimumCoordinatesTextArea.setPrefWidth(50); - Button changeMinimumCoordinatesButton = new Button("Change Min. Coordinates"); - minCoordinatesBox.getChildren().addAll(minCoordinatesLabel,minimumCoordinatesTextArea,changeMinimumCoordinatesButton); - // Maximum Coordinates GUI - VBox maxCoordinatesBox = new VBox(); - Label maxCoordinatesLabel = new Label("Max Coordinates"); - TextArea maximumCoordinatesTextArea = new TextArea(); - maximumCoordinatesTextArea.setPrefHeight(5); - maximumCoordinatesTextArea.setPrefWidth(50); - Button changeMaximumCoordinatesButton = new Button("Change Max Coordinates"); - maxCoordinatesBox.getChildren().addAll(maxCoordinatesLabel,maximumCoordinatesTextArea,changeMaximumCoordinatesButton); - // Fill parameter box - parameterBox.getChildren().addAll(stepCountBox, minCoordinatesBox, maxCoordinatesBox); - parameterBox.setPadding(new Insets(10)); - - // Add basic control buttons - sideMenu.getChildren().addAll(startButton,stopButton,newButton,clearButton); - // Add fractal radio buttons - sideMenu.getChildren().addAll(sierpinskiRadioButton, barnsleyRadioButton, juliaRadioButton, improvedBarnsleyButton); - // Add parameter VBox - sideMenu.getChildren().add(parameterBox); - // Add file buttons - sideMenu.getChildren().addAll(loadFractalFromFileButton,writeFractalToFileButton); - // Add quit button - sideMenu.getChildren().add(quitButton); - // Add padding - sideMenu.setPadding(new Insets(10)); - - this.borderPane = new BorderPane(); - this.borderPane.setCenter(imageView); - this.borderPane.setRight(sideMenu); - imageView.setFocusTraversable(true); - sideMenu.setFocusTraversable(false); - borderPane.setFocusTraversable(false); - - } - - public void drawChaosGame(){ - ChaosCanvas canvas = game.getCanvas(); - - - game.runSteps(100000); - - // Test implementation for drawing fractals - int[][] betaArray = canvas.getCanvasArray(); - for (int i = 0; i < canvas.getWidth(); i++) { - for (int j = 0; j < canvas.getHeight(); j++) { - if (betaArray[i][j] == 1) { - pixelWriter.setColor(j,i,Color.BLACK); - } - } - - } - - } - - public int getWidth(){ - return this.width; - } - public int getHeight(){ - return this.height; - } - public ImageView getImageView(){ - return this.imageView; - } - - public void setCurrentLine(int currentLine) { - this.currentLine = currentLine; - } - - public void setPixelWriter(PixelWriter pixelWriter) { - this.pixelWriter = pixelWriter; - } - - public void setImageViewFromImage(Image inputView) { - this.imageView.setImage(inputView); - } - - /** - * Update the description of the chaos game. - * TODO: this method may need to be changed depending on how we implement the UI. - * - * @param description the description. - */ - @Override - public void updateDescription(ChaosGameDescription description) { - this.game.setDescription(description); - } - - /** - * Update the observer based on changes to the chaos game. - * TODO: this method may need to be changed depending on how we implement the UI. The update method may need to be split. - * - * @param game the game this observer is monitoring. - */ - @Override - public void update(ChaosGame game) { - //drawChaosGame(); - } - - -} diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java index 402051123b9d4d63d54b51e178b1c7172da97fea..e956a4f580081ca591abdba502bf203d5e644846 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java @@ -11,34 +11,70 @@ import javafx.scene.transform.Affine; */ public class ChaosGameImageView extends ImageView { - private final ChaosGameGuiView controller; + /** + * The controller for this class: a chaos game GUI view. + */ + private final ChaosGameGui controller; + + /** + * Affine initialised to the identity matrix. + */ private final Affine transform = new Affine(); - private double lastCentreX; - private double lastCentreY; + private double centreX; + private double centreY; + + /** + * The starting x and y positions upon a mouse event. + */ private double startX; private double startY; - public ChaosGameImageView(ChaosGameGuiView controller) { - this.setOnScroll(this::zoom); + /** + * The factor representing the magnitude of the current zoom. + */ + private double zoomFactor = 1.0; + + /** + * Integer representing how many recursive levels of QuadTrees + * are required to get to the current zoom magnitude. + */ + private int zoomLevel = 0; + + + + /** + * Constructor for the ChaosGameImageView. + * + * @param controller the GUI which controls this image view. + */ + public ChaosGameImageView(ChaosGameGui controller) { + this.setOnScroll(this::userZoom); this.setOnMousePressed(this::mousePressed); this.setOnMouseDragged(this::mouseDragged); - this.setOnMouseReleased(this::mouseReleased); + //this.setOnMouseReleased(this::mouseReleased); this.getTransforms().add(transform); this.controller = controller; - this.lastCentreX = (float) controller.getWidth() / 2; - this.lastCentreY = (float) -controller.getHeight() / 2; + //this.lastCentreX = (float) controller.getWidth() / 2; + //this.lastCentreY = (float) -controller.getHeight() / 2; //this.setStyle("-fx-background-color: white;"); } + /** + * Get the current zoom level. + * + * @return the integer representing the zoom level. + */ + public int getZoomLevel() {return this.zoomLevel; } + /** * Zooms the image view in or out based on the scroll event. * * @param event the event. */ - private synchronized void zoom(ScrollEvent event) { - double zoomFactor = event.getDeltaY() > 0 ? 1.20 : 1 / 1.05; + private synchronized void userZoom(ScrollEvent event) { + double newZoomFactor = event.getDeltaY() > 0 ? 1.10 : 1 / 1.05; try { // Get the old values double oldScaleX = transform.getMxx(); @@ -47,8 +83,8 @@ public class ChaosGameImageView extends ImageView { double oldTranslateY = transform.getTy(); // Compute the new values - double newScaleX = oldScaleX * zoomFactor; - double newScaleY = oldScaleY * zoomFactor; + double newScaleX = oldScaleX * newZoomFactor; + double newScaleY = oldScaleY * newZoomFactor; double newTranslateX = oldTranslateX - (event.getX() * (newScaleX - oldScaleX)); double newTranslateY = oldTranslateY - (event.getY() * (newScaleY - oldScaleY)); @@ -57,16 +93,63 @@ public class ChaosGameImageView extends ImageView { transform.setMyy(newScaleY); transform.setTx(newTranslateX); transform.setTy(newTranslateY); + + this.zoomFactor *= newZoomFactor; + this.zoomLevel = (int) (Math.log(this.zoomFactor) / Math.log(4)); // Update zoom level. + + } catch (Exception e) { PopupManager.displayError("Zoom error", e.getMessage()); } } + public void fixedZoom(double newZoomFactor) { + try { + // Get the old values + double oldScaleX = transform.getMxx(); + double oldScaleY = transform.getMyy(); + double oldTranslateX = transform.getTx(); + double oldTranslateY = transform.getTy(); + + // Compute the new values + double newScaleX = oldScaleX * newZoomFactor; + double newScaleY = oldScaleY * newZoomFactor; + double newTranslateX = oldTranslateX - (this.getWidth() / 2 * (newScaleX - oldScaleX)); + double newTranslateY = oldTranslateY - (this.getHeight() / 2 * (newScaleY - oldScaleY)); + + // Update the transform + transform.setMxx(newScaleX); + transform.setMyy(newScaleY); + transform.setTx(newTranslateX); + transform.setTy(newTranslateY); + + this.zoomFactor *= newZoomFactor; + this.zoomLevel = (int) (Math.log(this.zoomFactor) / Math.log(4)); // Update zoom level. + + } catch (Exception e) { + PopupManager.displayError("Zoom error", e.getMessage()); + } + } +// TODO: remove if unused + private void updateController() { + this.controller.updateDetail(this.zoomLevel, this.centreX, this.centreY); + } + + /** + * Gets mouse cursor position data when mouse button is pressed. + * + * @param event the mouse event. + */ private synchronized void mousePressed(javafx.scene.input.MouseEvent event) { startX = event.getX(); startY = event.getY(); } + /** + * Drags the image view based on the mouse cursor position. + * + * @param event the mouse event. + */ private synchronized void mouseDragged(javafx.scene.input.MouseEvent event) { double deltaX = event.getX() - startX; double deltaY = event.getY() - startY; @@ -75,13 +158,14 @@ public class ChaosGameImageView extends ImageView { transform.setTy(transform.getTy() + deltaY); } - private synchronized void mouseReleased(javafx.scene.input.MouseEvent event) { - double deltaX = event.getX() - startX; - double deltaY = event.getY() - startY; - + public int getWidth() { + return (int) this.getImage().getWidth(); + } - lastCentreX += deltaX; - lastCentreY += deltaY; + public int getHeight() { + return (int) this.getImage().getHeight(); } + public Affine getTransform() {return this.transform; } + } diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameObserver.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameObserver.java index 40b7fcab45df00e2e4ace24e4aad84eefb6cf313..5eb1f0888f1a289801c797e4f626a307b1857adc 100644 --- a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameObserver.java +++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameObserver.java @@ -1,6 +1,7 @@ package edu.ntnu.stud.chaosgame.view; import edu.ntnu.stud.chaosgame.controller.game.ChaosGame; +import edu.ntnu.stud.chaosgame.controller.game.ChaosCanvas; import edu.ntnu.stud.chaosgame.controller.game.ChaosGameDescription; /** @@ -8,23 +9,21 @@ import edu.ntnu.stud.chaosgame.controller.game.ChaosGameDescription; * TODO: Do we want to have separate update methods for the canvas and description or just one for the whole game? (likely the latter) */ public interface ChaosGameObserver { -// TODO: Create interface - /** - * Perform update of the ChaosCanvas. + * Perform update of the ChaosGameDescription. * - * @param canvas the canvas. + * @param index the description's index in a list of descriptions. */ - //void updateCanvas(Chaosanvas canvas); + //void updateDescription(ChaosGameDescription description); + void updateDescription(int index); /** - * Perform update of the ChaosGameDescription. + * Update the ChaosCanvas. * - * @param description the description. + * @param canvas the canvas to update with. */ - //void updateDescription(ChaosGameDescription description); - void updateDescription(ChaosGameDescription description); + void updateCanvas(ChaosCanvas canvas); /** * Update the observer based on changes to the chaos game. diff --git a/src/test/java/edu/ntnu/stud/chaosgame/game/ChaosCanvasTest.java b/src/test/java/edu/ntnu/stud/chaosgame/game/ChaosCanvasTest.java index 19a48136b164bbed5d7a6ed0d25afbd8f43d4ea0..9f7e32f60b6de28e6e3b103826e2dbb6e77f9a9f 100644 --- a/src/test/java/edu/ntnu/stud/chaosgame/game/ChaosCanvasTest.java +++ b/src/test/java/edu/ntnu/stud/chaosgame/game/ChaosCanvasTest.java @@ -56,7 +56,7 @@ public class ChaosCanvasTest { @Test void shouldPutPixel() { Vector2D point = new Vector2D(0.2, 0.3); - this.canvas.putPixel(point); + this.canvas.putPixel(point, false); // Test whether new point was added Assertions.assertEquals(this.canvas.getPixel(point), 1); @@ -65,6 +65,24 @@ public class ChaosCanvasTest { Assertions.assertEquals(this.canvas.getPixel(new Vector2D(0.1, 0.4)), 0); } + /** + * Tests whether putting pixels in the intensity array works as expected, incrementing + * by one for each time the pixel is visited. + */ + @Test + void shouldPutPixelAndIntensity() { + Vector2D point = new Vector2D(0.2, 0.3); + this.canvas.putPixel(point, true); + this.canvas.putPixel(point, true); + + // Test whether new point was added + Assertions.assertEquals(this.canvas.getPixel(point), 1); + Assertions.assertEquals(this.canvas.getIntensityPixel(point), 2); + + // Ensure another, arbitrary point was not also added + Assertions.assertEquals(this.canvas.getPixel(new Vector2D(0.1, 0.4)), 0); + } + /** * Test whether clearing the canvas works as expected. */ @@ -73,7 +91,7 @@ public class ChaosCanvasTest { // Put pixels throughout a part of the canvas. for (int i = 1; i < 101; i++) { - this.canvas.putPixel(new Vector2D( 1.0 / i, 1.0 / i)); + this.canvas.putPixel(new Vector2D( 1.0 / i, 1.0 / i), false); // Check that the of the points where a pixel was added, are not equal to 0. Assertions.assertNotEquals(0, this.canvas.getPixel(new Vector2D(1.0 / i, 1.0 / i)));