292 lines
12 KiB
Java
292 lines
12 KiB
Java
package ch.fritteli.maze.generator.renderer.pdf;
|
|
|
|
import ch.fritteli.maze.generator.model.Direction;
|
|
import ch.fritteli.maze.generator.model.Maze;
|
|
import ch.fritteli.maze.generator.model.Position;
|
|
import ch.fritteli.maze.generator.model.Tile;
|
|
import io.vavr.control.Option;
|
|
import java.awt.BasicStroke;
|
|
import java.awt.Color;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import lombok.NonNull;
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.Value;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
|
import org.apache.pdfbox.pdmodel.PDPage;
|
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
@RequiredArgsConstructor
|
|
@Slf4j
|
|
class Generator {
|
|
|
|
@NonNull
|
|
private final Maze maze;
|
|
|
|
@NonNull
|
|
public ByteArrayOutputStream generate() {
|
|
final float pageWidth = this.maze.getWidth() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
|
final float pageHeight = this.maze.getHeight() * PDFRenderer.SCALE + 2 * PDFRenderer.MARGIN;
|
|
|
|
final PDDocument pdDocument = new PDDocument();
|
|
final PDDocumentInformation info = new PDDocumentInformation();
|
|
info.setTitle("Maze %sx%s, ID %s".formatted(this.maze.getWidth(), this.maze.getHeight(), this.maze.getRandomSeed()));
|
|
pdDocument.setDocumentInformation(info);
|
|
final PDPage puzzlePage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
|
final PDPage solutionPage = new PDPage(new PDRectangle(pageWidth, pageHeight));
|
|
pdDocument.addPage(puzzlePage);
|
|
pdDocument.addPage(solutionPage);
|
|
try (final PDPageContentStream puzzlePageContentStream = new PDPageContentStream(pdDocument, puzzlePage);
|
|
final PDPageContentStream solutionPageContentStream = new PDPageContentStream(pdDocument, solutionPage)) {
|
|
setUpPageContentStream(puzzlePageContentStream);
|
|
setUpPageContentStream(solutionPageContentStream);
|
|
this.drawHorizontalLines(puzzlePageContentStream, solutionPageContentStream);
|
|
this.drawVerticalLines(puzzlePageContentStream, solutionPageContentStream);
|
|
this.drawSolution(solutionPageContentStream);
|
|
} catch (IOException e) {
|
|
log.error("Error while rendering PDF document", e);
|
|
}
|
|
final ByteArrayOutputStream output = new ByteArrayOutputStream();
|
|
try {
|
|
pdDocument.save(output);
|
|
pdDocument.close();
|
|
} catch (IOException e) {
|
|
log.error("Error while writing PDF data", e);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
private void setUpPageContentStream(@NonNull final PDPageContentStream pageContentStream) throws IOException {
|
|
pageContentStream.setLineCapStyle(BasicStroke.CAP_ROUND);
|
|
pageContentStream.setLineJoinStyle(BasicStroke.JOIN_ROUND);
|
|
pageContentStream.setLineWidth(1.0f);
|
|
pageContentStream.setStrokingColor(Color.BLACK);
|
|
pageContentStream.setNonStrokingColor(Color.BLACK);
|
|
}
|
|
|
|
private void drawHorizontalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
|
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
|
Coordinate coordinate = new Coordinate(0f, 0f);
|
|
// Draw the TOP borders of all tiles.
|
|
for (int y = 0; y < this.maze.getHeight(); y++) {
|
|
boolean isPainting = false;
|
|
coordinate = coordinate.withY(y);
|
|
for (int x = 0; x < this.maze.getWidth(); x++) {
|
|
final Tile currentTile = this.maze.getTileAt(x, y).get();
|
|
coordinate = coordinate.withX(x);
|
|
if (currentTile.hasWallAt(Direction.TOP)) {
|
|
if (!isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.moveTo(coordinate.getX(), coordinate.getY());
|
|
}
|
|
isPainting = true;
|
|
}
|
|
} else {
|
|
if (isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
isPainting = false;
|
|
}
|
|
}
|
|
}
|
|
if (isPainting) {
|
|
coordinate = coordinate.withX(this.maze.getWidth());
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
}
|
|
}
|
|
// Draw the BOTTOM border of the last row of tiles.
|
|
boolean isPainting = false;
|
|
int y = this.maze.getHeight();
|
|
coordinate = coordinate.withY(this.maze.getHeight());
|
|
for (int x = 0; x < this.maze.getWidth(); x++) {
|
|
final Tile currentTile = this.maze.getTileAt(x, y - 1).get();
|
|
coordinate = coordinate.withX(x);
|
|
if (currentTile.hasWallAt(Direction.BOTTOM)) {
|
|
if (!isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.moveTo(coordinate.getX(), coordinate.getY());
|
|
}
|
|
isPainting = true;
|
|
}
|
|
} else {
|
|
if (isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
isPainting = false;
|
|
}
|
|
}
|
|
}
|
|
if (isPainting) {
|
|
coordinate = coordinate.withX(this.maze.getWidth());
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void drawVerticalLines(@NonNull final PDPageContentStream... contentStreams) throws IOException {
|
|
// PDF has the origin in the lower left corner. We want it in the upper left corner, hence some magic is required.
|
|
Coordinate coordinate = new Coordinate(0f, 0f);
|
|
// Draw the LEFT borders of all tiles.
|
|
for (int x = 0; x < this.maze.getWidth(); x++) {
|
|
boolean isPainting = false;
|
|
coordinate = coordinate.withX(x);
|
|
for (int y = 0; y < this.maze.getHeight(); y++) {
|
|
final Tile currentTile = this.maze.getTileAt(x, y).get();
|
|
coordinate = coordinate.withY(y);
|
|
if (currentTile.hasWallAt(Direction.LEFT)) {
|
|
if (!isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.moveTo(coordinate.getX(), coordinate.getY());
|
|
}
|
|
isPainting = true;
|
|
}
|
|
} else {
|
|
if (isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
isPainting = false;
|
|
}
|
|
}
|
|
}
|
|
if (isPainting) {
|
|
coordinate = coordinate.withY(this.maze.getHeight());
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
}
|
|
}
|
|
// Draw the RIGHT border of the last column of tiles.
|
|
boolean isPainting = false;
|
|
int x = this.maze.getWidth();
|
|
coordinate = coordinate.withX(this.maze.getWidth());
|
|
for (int y = 0; y < this.maze.getHeight(); y++) {
|
|
final Tile currentTile = this.maze.getTileAt(x - 1, y).get();
|
|
coordinate = coordinate.withY(y);
|
|
if (currentTile.hasWallAt(Direction.RIGHT)) {
|
|
if (!isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.moveTo(coordinate.getX(), coordinate.getY());
|
|
}
|
|
isPainting = true;
|
|
}
|
|
} else {
|
|
if (isPainting) {
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
isPainting = false;
|
|
}
|
|
}
|
|
}
|
|
if (isPainting) {
|
|
coordinate = coordinate.withY(this.maze.getHeight());
|
|
for (final PDPageContentStream contentStream : contentStreams) {
|
|
contentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
contentStream.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void drawSolution(@NonNull final PDPageContentStream pageContentStream) throws IOException {
|
|
// Draw the solution in red
|
|
pageContentStream.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 Position end = this.maze.getEnd();
|
|
Position currentPosition = this.maze.getStart();
|
|
Position previousPosition = null;
|
|
SolutionCoordinate coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY());
|
|
pageContentStream.moveTo(coordinate.getX(), coordinate.getY());
|
|
do {
|
|
Position newCurrent = this.findNextSolutionPosition(previousPosition, currentPosition);
|
|
previousPosition = currentPosition;
|
|
currentPosition = newCurrent;
|
|
coordinate = new SolutionCoordinate(currentPosition.getX(), currentPosition.getY());
|
|
pageContentStream.lineTo(coordinate.getX(), coordinate.getY());
|
|
} while (!currentPosition.equals(end));
|
|
pageContentStream.stroke();
|
|
}
|
|
|
|
@NonNull
|
|
private Position findNextSolutionPosition(@Nullable final Position previousPosition, @NonNull final Position currentPosition) {
|
|
final Tile currentTile = this.maze.getTileAt(currentPosition).get();
|
|
for (final Direction direction : Direction.values()) {
|
|
if (!currentTile.hasWallAt(direction)) {
|
|
final Position position = currentPosition.move(direction);
|
|
final Option<Tile> tileAtPosition = this.maze.getTileAt(position);
|
|
if (position.equals(previousPosition)) {
|
|
continue;
|
|
}
|
|
if (tileAtPosition.map(Tile::isSolution).getOrElse(false)) {
|
|
return position;
|
|
}
|
|
}
|
|
}
|
|
throw new IllegalStateException("We *SHOULD* never have gotten here. ... famous last words ...");
|
|
}
|
|
|
|
@Value
|
|
private class Coordinate {
|
|
|
|
float x;
|
|
float y;
|
|
|
|
public Coordinate(final float x, final float y) {
|
|
this.x = x;
|
|
this.y = y;
|
|
}
|
|
|
|
public Coordinate withX(final int x) {
|
|
return new Coordinate(calcX(x), this.y);
|
|
}
|
|
|
|
private float calcX(final int x) {
|
|
return x * PDFRenderer.SCALE + PDFRenderer.MARGIN;
|
|
}
|
|
|
|
public Coordinate withY(final int y) {
|
|
return new Coordinate(this.x, calcY(y));
|
|
}
|
|
|
|
private float calcY(final int y) {
|
|
return (Generator.this.maze.getHeight() - y) * PDFRenderer.SCALE + PDFRenderer.MARGIN;
|
|
}
|
|
}
|
|
|
|
@Value
|
|
private class SolutionCoordinate {
|
|
|
|
float x;
|
|
float y;
|
|
|
|
public SolutionCoordinate(final int x, final int y) {
|
|
this.x = calcX(x);
|
|
this.y = calcY(y);
|
|
}
|
|
|
|
private float calcX(final int x) {
|
|
return x * PDFRenderer.SCALE + PDFRenderer.SCALE / 2 + PDFRenderer.MARGIN;
|
|
}
|
|
|
|
private float calcY(final int y) {
|
|
return (Generator.this.maze.getHeight() - y) * PDFRenderer.SCALE - PDFRenderer.SCALE / 2 + PDFRenderer.MARGIN;
|
|
}
|
|
}
|
|
}
|