From 1671807bff67dea4a1bdf3851be5642446414fa4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?H=C3=A5vard=20Daleng?=
 <142524365+MrMarHVD@users.noreply.github.com>
Date: Fri, 12 Apr 2024 16:13:04 +0200
Subject: [PATCH] Implemented working zoom and pan functionality.

---
 .../java/edu/ntnu/stud/chaosgame/Main.java    | 11 +--
 .../chaosgame/controller/PopupManager.java    | 21 +++++
 ...GameGUIView.java => ChaosGameGuiView.java} | 35 ++++++--
 .../chaosgame/view/ChaosGameImageView.java    | 87 +++++++++++++++++++
 4 files changed, 136 insertions(+), 18 deletions(-)
 create mode 100644 src/main/java/edu/ntnu/stud/chaosgame/controller/PopupManager.java
 rename src/main/java/edu/ntnu/stud/chaosgame/view/{ChaosGameGUIView.java => ChaosGameGuiView.java} (95%)
 create mode 100644 src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java

diff --git a/src/main/java/edu/ntnu/stud/chaosgame/Main.java b/src/main/java/edu/ntnu/stud/chaosgame/Main.java
index 2019418..58d22a0 100644
--- a/src/main/java/edu/ntnu/stud/chaosgame/Main.java
+++ b/src/main/java/edu/ntnu/stud/chaosgame/Main.java
@@ -1,17 +1,10 @@
 package edu.ntnu.stud.chaosgame;
 
-import edu.ntnu.stud.chaosgame.controller.game.ChaosGameDescription;
-import edu.ntnu.stud.chaosgame.controller.game.ChaosGameFileHandler;
-import edu.ntnu.stud.chaosgame.controller.game.ChaosCanvas;
-import edu.ntnu.stud.chaosgame.controller.game.ChaosGame;
-import edu.ntnu.stud.chaosgame.model.generators.ChaosGameDescriptionFactory;
-import edu.ntnu.stud.chaosgame.view.ChaosGameGUIView;
+import edu.ntnu.stud.chaosgame.view.ChaosGameGuiView;
 import javafx.application.Application;
 import javafx.stage.Stage;
 
 import java.io.IOException;
-import java.util.Objects;
-import java.util.Scanner;
 
 /**
  * Main class for the Chaos Game application.
@@ -20,7 +13,7 @@ public class Main extends Application {
 
   @Override
   public void start(Stage primaryStage) throws IOException {
-    ChaosGameGUIView view = new ChaosGameGUIView(primaryStage);
+    ChaosGameGuiView view = new ChaosGameGuiView(primaryStage);
 
   }
 
diff --git a/src/main/java/edu/ntnu/stud/chaosgame/controller/PopupManager.java b/src/main/java/edu/ntnu/stud/chaosgame/controller/PopupManager.java
new file mode 100644
index 0000000..5cec0d7
--- /dev/null
+++ b/src/main/java/edu/ntnu/stud/chaosgame/controller/PopupManager.java
@@ -0,0 +1,21 @@
+package edu.ntnu.stud.chaosgame.controller;
+
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+
+public class PopupManager {
+
+  /**
+   * Display a basic error.
+   *
+   * @param header the header of the popup message.
+   * @param content the content of the popup message.
+   */
+  public static void displayError(String header,  String content) {
+    Alert alert = new Alert(AlertType.ERROR);
+    alert.setHeaderText(header);
+    alert.setContentText(content);
+    alert.showAndWait();
+  }
+
+}
diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGUIView.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java
similarity index 95%
rename from src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGUIView.java
rename to src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java
index 7aae505..1777cdb 100644
--- a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGUIView.java
+++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameGuiView.java
@@ -10,7 +10,6 @@ import javafx.animation.KeyFrame;
 import javafx.animation.Timeline;
 import javafx.application.Platform;
 import javafx.geometry.Insets;
-import javafx.geometry.Rectangle2D;
 import javafx.scene.Scene;
 import javafx.scene.control.*;
 import javafx.scene.image.Image;
@@ -26,7 +25,7 @@ import javafx.util.Duration;
 import java.io.IOException;
 import java.util.concurrent.atomic.AtomicReference;
 
-public class ChaosGameGUIView implements ChaosGameObserver {
+public class ChaosGameGuiView implements ChaosGameObserver {
   private int currentLine = 0;
 
   /**
@@ -57,7 +56,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
   /**
    * The ImageView for the GUI..
    */
-  private ImageView imageView;
+  private ChaosGameImageView imageView;
 
   /**
    * The Scene for the GUI..
@@ -114,7 +113,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
   private RadioButton improvedBarnsleyButton;
 
 
-  public ChaosGameGUIView(Stage primaryStage) throws IOException {
+  public ChaosGameGuiView(Stage primaryStage) throws IOException {
 
     this.initializeComponents();
 
@@ -195,21 +194,22 @@ public class ChaosGameGUIView implements ChaosGameObserver {
    */
   private void initializeImageView() {
     // Image view
-    this.imageView = new ImageView();
+    this.imageView = new ChaosGameImageView(this);
     width = 1000;
     height = 1000;
     WritableImage writableImage = new WritableImage(width, height);
     pixelWriter = writableImage.getPixelWriter();
     this.imageView.setImage(writableImage);
 
-    imageView.setOnScroll(event -> {
+    this.fillImageView();
+    /*imageView.setOnScroll(event -> {
       double zoomFactor = 1.05; // This is the zoom factor per scroll
-      double deltaY = event.getDeltaY();
+      double movement = event.getDeltaY();
 
       imageView.setTranslateX(imageView.getTranslateX() - event.getDeltaX());
       imageView.setTranslateY(imageView.getTranslateY() - event.getDeltaY());
 
-      if (deltaY > 0) {
+      if (movement > 0) {
         imageView.setScaleX(imageView.getScaleX() * zoomFactor);
         imageView.setScaleY(imageView.getScaleY() * zoomFactor);
       } else {
@@ -224,7 +224,20 @@ public class ChaosGameGUIView implements ChaosGameObserver {
       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);
+      }
+    }
 
   }
 
@@ -255,6 +268,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
       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
@@ -269,6 +283,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
       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(1000, 1000, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords()));
@@ -282,6 +297,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
       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(1000, 1000, this.descriptionRef.get().getMinCoords(), this.descriptionRef.get().getMaxCoords()));
@@ -294,6 +310,7 @@ public class ChaosGameGUIView implements ChaosGameObserver {
       WritableImage newWritableImage = new WritableImage(width, height);
       setPixelWriter(newWritableImage.getPixelWriter());
       setImageViewFromImage(newWritableImage);
+      this.fillImageView();
       canvas.clearCanvas();
 
       // Test
diff --git a/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java
new file mode 100644
index 0000000..4020511
--- /dev/null
+++ b/src/main/java/edu/ntnu/stud/chaosgame/view/ChaosGameImageView.java
@@ -0,0 +1,87 @@
+package edu.ntnu.stud.chaosgame.view;
+
+import edu.ntnu.stud.chaosgame.controller.PopupManager;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.ScrollEvent;
+import javafx.scene.transform.Affine;
+
+/**
+ * This class extends ImageView to implement proper zooming and panning
+ * according to the requirements of the chaos game.
+ */
+public class ChaosGameImageView extends ImageView {
+
+  private final ChaosGameGuiView controller;
+  private final Affine transform = new Affine();
+
+  private double lastCentreX;
+  private double lastCentreY;
+  private double startX;
+  private double startY;
+
+  public ChaosGameImageView(ChaosGameGuiView controller) {
+    this.setOnScroll(this::zoom);
+    this.setOnMousePressed(this::mousePressed);
+    this.setOnMouseDragged(this::mouseDragged);
+    this.setOnMouseReleased(this::mouseReleased);
+    this.getTransforms().add(transform);
+    this.controller = controller;
+    this.lastCentreX = (float) controller.getWidth() / 2;
+    this.lastCentreY = (float) -controller.getHeight() / 2;
+
+    //this.setStyle("-fx-background-color: white;");
+  }
+
+  /**
+   * 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;
+    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 * zoomFactor;
+      double newScaleY = oldScaleY * zoomFactor;
+      double newTranslateX = oldTranslateX - (event.getX() * (newScaleX - oldScaleX));
+      double newTranslateY = oldTranslateY - (event.getY() * (newScaleY - oldScaleY));
+
+      // Update the transform
+      transform.setMxx(newScaleX);
+      transform.setMyy(newScaleY);
+      transform.setTx(newTranslateX);
+      transform.setTy(newTranslateY);
+    } catch (Exception e) {
+      PopupManager.displayError("Zoom error", e.getMessage());
+    }
+  }
+
+  private synchronized void mousePressed(javafx.scene.input.MouseEvent event) {
+    startX = event.getX();
+    startY = event.getY();
+  }
+
+  private synchronized void mouseDragged(javafx.scene.input.MouseEvent event) {
+    double deltaX = event.getX() - startX;
+    double deltaY = event.getY() - startY;
+
+    transform.setTx(transform.getTx() + deltaX);
+    transform.setTy(transform.getTy() + deltaY);
+  }
+
+  private synchronized void mouseReleased(javafx.scene.input.MouseEvent event) {
+    double deltaX = event.getX() - startX;
+    double deltaY = event.getY() - startY;
+
+
+    lastCentreX += deltaX;
+    lastCentreY += deltaY;
+  }
+
+}
-- 
GitLab