From 911aa35577d36e5eeeacbdcbf23e72fba0c5a345 Mon Sep 17 00:00:00 2001
From: Manuel Friedli <manuel@fritteli.ch>
Date: Sat, 3 Oct 2020 23:09:50 +0200
Subject: [PATCH] Add a second page with the solution to the PDF.

---
 .../java/ch/fritteli/labyrinth/Labyrinth.java |   2 +
 .../ch/fritteli/labyrinth/PDFRenderer.java    | 102 ++++++++++++++----
 2 files changed, 82 insertions(+), 22 deletions(-)

diff --git a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java
index 5e5be59..b5c6244 100644
--- a/src/main/java/ch/fritteli/labyrinth/Labyrinth.java
+++ b/src/main/java/ch/fritteli/labyrinth/Labyrinth.java
@@ -13,7 +13,9 @@ public class Labyrinth {
     private final int width;
     @Getter
     private final int height;
+    @Getter
     private final Position start;
+    @Getter
     private final Position end;
 
     public Labyrinth(final int width, final int height) {
diff --git a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java b/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java
index 5dad450..7b2d6ae 100644
--- a/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java
+++ b/src/main/java/ch/fritteli/labyrinth/PDFRenderer.java
@@ -5,12 +5,16 @@ import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDPage;
 import org.apache.pdfbox.pdmodel.PDPageContentStream;
 import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.jetbrains.annotations.Nullable;
 
 import java.awt.*;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 
 public class PDFRenderer implements Renderer<byte[]> {
+    private static final float MARGIN = 10;
+    private static final float SCALE = 10;
+
     private PDFRenderer() {
 
     }
@@ -20,13 +24,15 @@ public class PDFRenderer implements Renderer<byte[]> {
         return new PDFRenderer();
     }
 
+    private static boolean isValid(@NonNull final Position position) {
+        return position.getX() >= 0 && position.getY() >= 0;
+    }
+
     @Override
     @NonNull
     public byte[] render(@NonNull final Labyrinth labyrinth) {
-        final float scale = 10;
-        final float margin = 5;
-        final float pageWidth = labyrinth.getWidth() * scale + 2 * margin;
-        final float pageHeight = labyrinth.getHeight() * scale + 2 * margin;
+        final float pageWidth = labyrinth.getWidth() * SCALE + 2 * MARGIN;
+        final float pageHeight = labyrinth.getHeight() * SCALE + 2 * MARGIN;
 
         final PDDocument pdDocument = new PDDocument();
         final PDPage page = new PDPage(new PDRectangle(pageWidth, pageHeight));
@@ -37,8 +43,22 @@ public class PDFRenderer implements Renderer<byte[]> {
             pdPageContentStream.setLineWidth(1.0f);
             pdPageContentStream.setStrokingColor(Color.BLACK);
             pdPageContentStream.setNonStrokingColor(Color.BLACK);
-            this.drawHorizonzalLines(labyrinth, pdPageContentStream, margin, scale);
-            this.drawVerticalLines(labyrinth, pdPageContentStream, margin, scale);
+            this.drawHorizonzalLines(labyrinth, pdPageContentStream);
+            this.drawVerticalLines(labyrinth, pdPageContentStream);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        final PDPage solution = new PDPage(new PDRectangle(pageWidth, pageHeight));
+        pdDocument.addPage(solution);
+        try (PDPageContentStream pdPageContentStream = new PDPageContentStream(pdDocument, solution)) {
+            pdPageContentStream.setLineCapStyle(BasicStroke.CAP_ROUND);
+            pdPageContentStream.setLineJoinStyle(BasicStroke.JOIN_MITER);
+            pdPageContentStream.setLineWidth(1.0f);
+            pdPageContentStream.setStrokingColor(Color.BLACK);
+            pdPageContentStream.setNonStrokingColor(Color.BLACK);
+            this.drawHorizonzalLines(labyrinth, pdPageContentStream);
+            this.drawVerticalLines(labyrinth, pdPageContentStream);
+            this.drawSolution(labyrinth, pdPageContentStream);
         } catch (IOException e) {
             e.printStackTrace();
         }
@@ -52,28 +72,29 @@ public class PDFRenderer implements Renderer<byte[]> {
         return output.toByteArray();
     }
 
-    private void drawHorizonzalLines(@NonNull final Labyrinth labyrinth, @NonNull final PDPageContentStream pdPageContentStream, final float margin, final float scale) throws IOException {
+    private void drawHorizonzalLines(@NonNull final Labyrinth labyrinth,
+                                     @NonNull final PDPageContentStream pdPageContentStream) throws IOException {
         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
-        final float labyrinthHeight = labyrinth.getHeight() * scale;
+        final float labyrinthHeight = labyrinth.getHeight() * SCALE;
         for (int y = 0; y < labyrinth.getHeight(); y++) {
             boolean isPainting = false;
             for (int x = 0; x < labyrinth.getWidth(); x++) {
                 final Tile currentTile = labyrinth.getTileAt(x, y);
                 if (currentTile.hasWallAt(Direction.TOP)) {
                     if (!isPainting) {
-                        pdPageContentStream.moveTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                        pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                         isPainting = true;
                     }
                 } else {
                     if (isPainting) {
-                        pdPageContentStream.lineTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                        pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                         pdPageContentStream.stroke();
                         isPainting = false;
                     }
                 }
             }
             if (isPainting) {
-                pdPageContentStream.lineTo(labyrinth.getWidth() * scale + margin, labyrinthHeight - y * scale + margin);
+                pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                 pdPageContentStream.stroke();
             }
         }
@@ -83,45 +104,46 @@ public class PDFRenderer implements Renderer<byte[]> {
             final Tile currentTile = labyrinth.getTileAt(x, y - 1);
             if (currentTile.hasWallAt(Direction.BOTTOM)) {
                 if (!isPainting) {
-                    pdPageContentStream.moveTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                    pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                     isPainting = true;
                 }
             } else {
                 if (isPainting) {
-                    pdPageContentStream.lineTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                    pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                     pdPageContentStream.stroke();
                     isPainting = false;
                 }
             }
         }
         if (isPainting) {
-            pdPageContentStream.lineTo(labyrinth.getWidth() * scale + margin, labyrinthHeight - labyrinth.getHeight() * scale + margin);
+            pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN);
             pdPageContentStream.stroke();
         }
     }
 
-    private void drawVerticalLines(@NonNull final Labyrinth labyrinth, @NonNull final PDPageContentStream pdPageContentStream, final float margin, final float scale) throws IOException {
+    private void drawVerticalLines(@NonNull final Labyrinth labyrinth,
+                                   @NonNull final PDPageContentStream pdPageContentStream) throws IOException {
         // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
-        final float labyrinthHeight = labyrinth.getHeight() * scale;
+        final float labyrinthHeight = labyrinth.getHeight() * SCALE;
         for (int x = 0; x < labyrinth.getWidth(); x++) {
             boolean isPainting = false;
             for (int y = 0; y < labyrinth.getHeight(); y++) {
                 final Tile currentTile = labyrinth.getTileAt(x, y);
                 if (currentTile.hasWallAt(Direction.LEFT)) {
                     if (!isPainting) {
-                        pdPageContentStream.moveTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                        pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                         isPainting = true;
                     }
                 } else {
                     if (isPainting) {
-                        pdPageContentStream.lineTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                        pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                         pdPageContentStream.stroke();
                         isPainting = false;
                     }
                 }
             }
             if (isPainting) {
-                pdPageContentStream.lineTo(x * scale + margin, labyrinthHeight - labyrinth.getHeight() * scale + margin);
+                pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN);
                 pdPageContentStream.stroke();
             }
         }
@@ -131,20 +153,56 @@ public class PDFRenderer implements Renderer<byte[]> {
             final Tile currentTile = labyrinth.getTileAt(x - 1, y);
             if (currentTile.hasWallAt(Direction.RIGHT)) {
                 if (!isPainting) {
-                    pdPageContentStream.moveTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                    pdPageContentStream.moveTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                     isPainting = true;
                 }
             } else {
                 if (isPainting) {
-                    pdPageContentStream.lineTo(x * scale + margin, labyrinthHeight - y * scale + margin);
+                    pdPageContentStream.lineTo(x * SCALE + MARGIN, labyrinthHeight - y * SCALE + MARGIN);
                     pdPageContentStream.stroke();
                     isPainting = false;
                 }
             }
         }
         if (isPainting) {
-            pdPageContentStream.lineTo(labyrinth.getWidth() * scale + margin, labyrinthHeight - labyrinth.getHeight() * scale + margin);
+            pdPageContentStream.lineTo(labyrinth.getWidth() * SCALE + MARGIN, labyrinthHeight - labyrinth.getHeight() * SCALE + MARGIN);
             pdPageContentStream.stroke();
         }
     }
+
+    private void drawSolution(@NonNull final Labyrinth labyrinth,
+                              @NonNull final PDPageContentStream pdPageContentStream) throws IOException {
+        // Draw the solution in red
+        pdPageContentStream.setStrokingColor(Color.RED);
+        // PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
+        final float labyrinthHeight = labyrinth.getHeight() * SCALE;
+        final Position end = labyrinth.getEnd();
+        Position currentPosition = labyrinth.getStart();
+        Position previousPosition = null;
+        pdPageContentStream.moveTo(MARGIN + currentPosition.getX() * SCALE + SCALE / 2, MARGIN + labyrinthHeight - (currentPosition.getY() * SCALE + SCALE / 2));
+        do {
+            Position newCurrent = this.findNextSolutionPosition(labyrinth, previousPosition, currentPosition);
+            previousPosition = currentPosition;
+            currentPosition = newCurrent;
+            pdPageContentStream.lineTo(MARGIN + currentPosition.getX() * SCALE + SCALE / 2, MARGIN + labyrinthHeight - (currentPosition.getY() * SCALE + SCALE / 2));
+        } while (!currentPosition.equals(end));
+        pdPageContentStream.stroke();
+    }
+
+    private Position findNextSolutionPosition(@NonNull final Labyrinth labyrinth, @Nullable final Position previousPosition, @NonNull final Position currentPosition) {
+        final Tile currentTile = labyrinth.getTileAt(currentPosition);
+        for (final Direction direction : Direction.values()) {
+            if (!currentTile.hasWallAt(direction)) {
+                final Position position = direction.translate(currentPosition);
+                if (position.equals(previousPosition) || !isValid(position)) {
+                    continue;
+                }
+                if (labyrinth.getTileAt(position).isSolution()) {
+                    return position;
+                }
+            }
+        }
+        // We *SHOULD* never get here. ... famous last words ...
+        return null;
+    }
 }