diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java new file mode 100644 index 0000000..7a3fa5c --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java @@ -0,0 +1,81 @@ +package io.github.applecommander.bastools.api.shapes; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.List; + +public class BitmapShape implements Shape { + public final List> grid = new ArrayList<>(); + public final Point origin = new Point(); + + public BitmapShape() { + this(0,0); + } + public BitmapShape(int height, int width) { + while (grid.size() < height) { + grid.add(newRow(width)); + } + } + + private List newRow(int width) { + List row = new ArrayList<>(); + while (row.size() < width) { + row.add(Boolean.FALSE); + } + return row; + } + + public void insertColumn() { + origin.y++; + for (List row : grid) { + row.add(0, Boolean.FALSE); + } + } + public void addColumn() { + for (List row : grid) { + row.add(Boolean.FALSE); + } + } + public void insertRow() { + origin.x++; + grid.add(0, newRow(getWidth())); + } + public void addRow() { + grid.add(newRow(getWidth())); + } + + public int getHeight() { + return grid.size(); + } + public int getWidth() { + return grid.isEmpty() ? 0 : grid.get(0).size(); + } + + public void plot(int x, int y) { + plot(x, y, Boolean.TRUE); + } + public void plot(int x, int y, Boolean pixel) { + if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) { + return; + } + grid.get(y).set(x, pixel); + } + + public Boolean get(int x, int y) { + if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) { + return Boolean.FALSE; + } + return grid.get(y).get(x); + } + + @Override + public BitmapShape toBitmap() { + return this; + } + + @Override + public VectorShape toVector() { + // TODO Auto-generated method stub + return null; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java new file mode 100644 index 0000000..4e89b8b --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java @@ -0,0 +1,6 @@ +package io.github.applecommander.bastools.api.shapes; + +public interface Shape { + public BitmapShape toBitmap(); + public VectorShape toVector(); +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java new file mode 100644 index 0000000..5550b44 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java @@ -0,0 +1,50 @@ +package io.github.applecommander.bastools.api.shapes; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter; + +public interface ShapeExporter { + /** Export a single shape to the OutputStream. */ + public void export(Shape shape, OutputStream outputStream); + /** Export a single shape to the File. */ + public default void export(Shape shape, File file) throws IOException { + Objects.requireNonNull(shape); + Objects.requireNonNull(file); + export(shape, file.toPath()); + } + /** Export a single shape to the Path. */ + public default void export(Shape shape, Path path) throws IOException { + Objects.requireNonNull(shape); + Objects.requireNonNull(path); + try (OutputStream outputStream = Files.newOutputStream(path)) { + export(shape, outputStream); + } + } + + /** Export the entire shape table to the OutputStream. */ + public void export(ShapeTable shapeTable, OutputStream outputStream); + /** Export the entire shape table to the File. */ + public default void export(ShapeTable shapeTable, File file) throws IOException { + Objects.requireNonNull(shapeTable); + Objects.requireNonNull(file); + export(shapeTable, file.toPath()); + } + /** Export the entire shape table to the Path. */ + public default void export(ShapeTable shapeTable, Path path) throws IOException { + Objects.requireNonNull(shapeTable); + Objects.requireNonNull(path); + try (OutputStream outputStream = Files.newOutputStream(path)) { + export(shapeTable, outputStream); + } + } + + public static TextShapeExporter.Builder text() { + return new TextShapeExporter.Builder(); + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java new file mode 100644 index 0000000..6c00e01 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeTable.java @@ -0,0 +1,76 @@ +package io.github.applecommander.bastools.api.shapes; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.github.applecommander.bastools.api.utils.Streams; + +public class ShapeTable { + public static ShapeTable read(byte[] data) { + Objects.requireNonNull(data); + ShapeTable shapeTable = new ShapeTable(); + ByteBuffer buf = ByteBuffer.wrap(data) + .order(ByteOrder.LITTLE_ENDIAN); + int count = Byte.toUnsignedInt(buf.get()); + // unused: + buf.get(); + for (int i = 0; i < count; i++) { + int offset = buf.getShort(); + // load empty shapes as empty... + if (offset == 0) { + shapeTable.shapes.add(new VectorShape()); + continue; + } + // defer to VectorShape to process bits + buf.mark(); + buf.position(offset); + shapeTable.shapes.add(VectorShape.from(buf)); + buf.reset(); + } + return shapeTable; + } + + public static ShapeTable read(InputStream inputStream) throws IOException { + Objects.requireNonNull(inputStream); + return read(Streams.toByteArray(inputStream)); + } + + public static ShapeTable read(File file) throws IOException { + Objects.requireNonNull(file); + return read(file.toPath()); + } + + public static ShapeTable read(Path path) throws IOException { + Objects.requireNonNull(path); + return read(Files.readAllBytes(path)); + } + + public final List shapes = new ArrayList<>(); + + public void write(OutputStream outputStream) throws IOException { + Objects.requireNonNull(outputStream); + // TODO + } + + public void write(File file) throws IOException { + Objects.requireNonNull(file); + try (OutputStream outputStream = new FileOutputStream(file)) { + write(file); + } + } + + public void write(Path path) throws IOException { + Objects.requireNonNull(path); + write(path.toFile()); + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorCommand.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorCommand.java new file mode 100644 index 0000000..88f478b --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorCommand.java @@ -0,0 +1,32 @@ +package io.github.applecommander.bastools.api.shapes; + +/** + * Represents all "plot vectors" available in an Applesoft shape table. + * + * @see + * Applesoft BASIC Programming Reference Manual + */ +public enum VectorCommand { + // Order here is specific to the encoding within the shape itself + MOVE_UP, MOVE_RIGHT, MOVE_DOWN, MOVE_LEFT, + PLOT_UP, PLOT_RIGHT, PLOT_DOWN, PLOT_LEFT; + + public final boolean plot; + public final int xmove; + public final int ymove; + + private VectorCommand() { + this.plot = (this.ordinal() & 0b100) != 0; + // up 0b00 + // right 0b01 + // down 0b10 + // left 0b11 + if ((this.ordinal() & 0b001) == 1) { + this.xmove = 2 - (this.ordinal() & 0b011); + this.ymove = 0; + } else { + this.xmove = 0; + this.ymove = (this.ordinal() & 0b011) - 1; + } + } +} \ No newline at end of file diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java new file mode 100644 index 0000000..b470ea0 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java @@ -0,0 +1,86 @@ +package io.github.applecommander.bastools.api.shapes; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class VectorShape implements Shape { + public static VectorShape from(ByteBuffer buf) { + Objects.requireNonNull(buf); + VectorShape shape = new VectorShape(); + + VectorCommand[] commands = VectorCommand.values(); + while (buf.hasRemaining()) { + int code = Byte.toUnsignedInt(buf.get()); + if (code == 0) break; + + int vector1 = code & 0b111; + int vector2 = (code >> 3) & 0b111; + int vector3 = (code >> 6) & 0b011; // Cannot plot + + shape.vectors.add(commands[vector1]); + + if (vector2 != 0 || vector3 != 0) { + shape.vectors.add(commands[vector2]); + + if (vector3 != 0) { + shape.vectors.add(commands[vector3]); + } + } + } + return shape; + } + + public final List vectors = new ArrayList<>(); + + public VectorShape moveUp() { return add(VectorCommand.MOVE_UP); } + public VectorShape moveRight() { return add(VectorCommand.MOVE_RIGHT); } + public VectorShape moveDown() { return add(VectorCommand.MOVE_DOWN); } + public VectorShape moveLeft() { return add(VectorCommand.MOVE_LEFT); } + public VectorShape plotUp() { return add(VectorCommand.PLOT_UP); } + public VectorShape plotRight() { return add(VectorCommand.PLOT_RIGHT); } + public VectorShape plotDown() { return add(VectorCommand.PLOT_DOWN); } + public VectorShape plotLeft() { return add(VectorCommand.PLOT_LEFT); } + + private VectorShape add(VectorCommand vectorCommand) { + this.vectors.add(vectorCommand); + return this; + } + + @Override + public BitmapShape toBitmap() { + BitmapShape shape = new BitmapShape(); + + int x = 0; + int y = 0; + for (VectorCommand command : vectors) { + if (command.plot) { + while (y < 0) { + shape.insertRow(); + y += 1; + } + while (y >= shape.getHeight()) { + shape.addRow(); + } + while (x < 0) { + shape.insertColumn(); + x += 1; + } + while (x >= shape.getWidth()) { + shape.addColumn(); + } + shape.plot(x,y); + } + x += command.xmove; + y += command.ymove; + } + + return shape; + } + + @Override + public VectorShape toVector() { + return this; + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/TextShapeExporter.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/TextShapeExporter.java new file mode 100644 index 0000000..7fcb91d --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/TextShapeExporter.java @@ -0,0 +1,245 @@ +package io.github.applecommander.bastools.api.shapes.exporters; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import io.github.applecommander.bastools.api.shapes.BitmapShape; +import io.github.applecommander.bastools.api.shapes.Shape; +import io.github.applecommander.bastools.api.shapes.ShapeExporter; +import io.github.applecommander.bastools.api.shapes.ShapeTable; + +public class TextShapeExporter implements ShapeExporter { + private int maxWidth = 80; + private BorderStrategy borderStrategy = BorderStrategy.BOX_DRAWING; + + /** Use the {@code Builder} to create a TextShapeExporter. */ + private TextShapeExporter() { } + + @Override + public void export(Shape shape, OutputStream outputStream) { + Objects.requireNonNull(shape); + Objects.requireNonNull(outputStream); + + BitmapShape b = shape.toBitmap(); + PrintWriter pw = new PrintWriter(outputStream); + drawTopLine(pw, 1, b.getWidth()); + + Queue bqueue = new LinkedList<>(Arrays.asList(b)); + drawRow(pw, bqueue, 1, b.getHeight(), b.getWidth()); + + drawBottomLine(pw, 1, b.getWidth()); + pw.flush(); + } + + @Override + public void export(ShapeTable shapeTable, OutputStream outputStream) { + Objects.requireNonNull(shapeTable); + Objects.requireNonNull(outputStream); + + List blist = shapeTable.shapes.stream() + .map(Shape::toBitmap) + .collect(Collectors.toList()); + int width = blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt(); + int height = blist.stream().mapToInt(BitmapShape::getHeight).max().getAsInt(); + + int columns = Math.max(1, this.maxWidth / width); + + PrintWriter pw = new PrintWriter(outputStream); + drawTopLine(pw, columns, width); + + Queue bqueue = new LinkedList<>(blist); + drawRow(pw, bqueue, columns, height, width); + while (!bqueue.isEmpty()) { + drawDividerLine(pw, columns, width); + drawRow(pw, bqueue, columns, height, width); + } + + drawBottomLine(pw, columns, width); + pw.flush(); + } + + private void drawTopLine(PrintWriter pw, int columns, int width) { + borderStrategy.topLeftCorner(pw); + borderStrategy.horizontalLine(pw, width); + for (int i=1; i bqueue, int columns, int height, int width) { + BitmapShape[] bshapes = new BitmapShape[columns]; + for (int i=0; i row = bshape.grid.size() > y ? bshape.grid.get(y) : new ArrayList<>(); + for (int x=0; x x ? row.get(x) : Boolean.FALSE; + if (bshape.origin.x == x && bshape.origin.y == y) { + pw.printf("%c", plot ? '*' : '+'); + } else { + pw.printf("%c", plot ? 'X' : '.'); + } + } + } + + public enum BorderStrategy { + /** No border but with spaces between shapes. Note the tricky newline in {@code dividerLeftEdge}. */ + NONE('\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', ' ', '\0', '\n', '\0', '\0'), + /** + * A border comprised of the box drawing characters. + * @see Wikipedia article on box characters + */ + BOX_DRAWING('\u2500', '\u2502', '\u250C', '\u2510', '\u2514', '\u2518', '\u252C', '\u2534', + '\u2502', '\u2500', '\u251C', '\u2524', '\u253C'), + /** A simple border based on plain ASCII characters. */ + ASCII_TEXT('-', '|', '+', '+', '+', '+', '+', '+', '|', '-', '+', '+', '+'); + + private final char horizontalLine; + private final char verticalLine; + private final char topLeftCorner; + private final char topRightCorner; + private final char bottomLeftCorner; + private final char bottomRightCorner; + private final char topDivider; + private final char bottomDivider; + private final char dividerVerticalLine; + private final char dividerHorizontalLine; + private final char dividerLeftEdge; + private final char dividerRightEdge; + private final char dividerMiddle; + + private BorderStrategy(char horizontalLine, char verticalLine, char topLeftCorner, char topRightCorner, + char bottomLeftCorner, char bottomRightCorner, char topDivider, char bottomDivider, + char dividerVerticalLine, char dividerHorizontalLine, char dividerLeftEdge, char dividerRightEdge, + char dividerMiddle) { + this.horizontalLine = horizontalLine; + this.verticalLine = verticalLine; + this.topLeftCorner = topLeftCorner; + this.topRightCorner = topRightCorner; + this.bottomLeftCorner = bottomLeftCorner; + this.bottomRightCorner = bottomRightCorner; + this.topDivider = topDivider; + this.bottomDivider = bottomDivider; + this.dividerVerticalLine = dividerVerticalLine; + this.dividerHorizontalLine = dividerHorizontalLine; + this.dividerLeftEdge = dividerLeftEdge; + this.dividerRightEdge = dividerRightEdge; + this.dividerMiddle = dividerMiddle; + } + + private void print(Consumer output, char ch) { + print(output, ch, 1); + } + private void print(Consumer output, char ch, int width) { + if (ch != '\0') { + output.accept(new String(new char[width]).replace('\0', ch)); + } + } + + public void horizontalLine(PrintWriter pw, int width) { + print(pw::print, horizontalLine, width); + } + public void verticalLine(PrintWriter pw) { + print(pw::print, verticalLine); + } + public void topLeftCorner(PrintWriter pw) { + print(pw::print, topLeftCorner); + } + public void topRightCorner(PrintWriter pw) { + print(pw::println, topRightCorner); + } + public void bottomLeftCorner(PrintWriter pw) { + print(pw::print, bottomLeftCorner); + } + public void bottomRightCorner(PrintWriter pw) { + print(pw::println, bottomRightCorner); + } + public void topDivider(PrintWriter pw) { + print(pw::print, topDivider); + } + public void bottomDivider(PrintWriter pw) { + print(pw::print, bottomDivider); + } + public void dividerVerticalLine(PrintWriter pw) { + print(pw::print, dividerVerticalLine); + } + public void dividerHorizontalLine(PrintWriter pw, int width) { + print(pw::print, dividerHorizontalLine, width); + } + public void dividerLeftEdge(PrintWriter pw) { + print(pw::print, dividerLeftEdge); + } + public void dividerRightEdge(PrintWriter pw) { + print(pw::println, dividerRightEdge); + } + public void dividerMiddle(PrintWriter pw) { + print(pw::print, dividerMiddle); + } + } + + public static class Builder { + private TextShapeExporter textShapeExporter = new TextShapeExporter(); + + public Builder maxWidth(int maxWidth) { + textShapeExporter.maxWidth = maxWidth; + return this; + } + public Builder noBorder() { + return borderStrategy(BorderStrategy.NONE); + } + public Builder asciiTextBorder() { + return borderStrategy(BorderStrategy.ASCII_TEXT); + } + public Builder boxDrawingBorder() { + return borderStrategy(BorderStrategy.BOX_DRAWING); + } + public Builder borderStrategy(BorderStrategy borderStrategy) { + textShapeExporter.borderStrategy = borderStrategy; + return this; + } + + public ShapeExporter build() { + return textShapeExporter; + } + } +} diff --git a/api/src/main/java/io/github/applecommander/bastools/api/utils/Streams.java b/api/src/main/java/io/github/applecommander/bastools/api/utils/Streams.java new file mode 100644 index 0000000..f1dd62d --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/utils/Streams.java @@ -0,0 +1,23 @@ +package io.github.applecommander.bastools.api.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class Streams { + private Streams() { /* Prevent construction */ } + + /** Utility method to read all bytes from an InputStream. */ + public static byte[] toByteArray(InputStream inputStream) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + while (true) { + byte[] buf = new byte[1024]; + int len = inputStream.read(buf); + if (len == -1) break; + outputStream.write(buf, 0, len); + } + outputStream.flush(); + return outputStream.toByteArray(); + } + +} diff --git a/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java b/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java new file mode 100644 index 0000000..d3fcb64 --- /dev/null +++ b/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java @@ -0,0 +1,143 @@ +package io.github.applecommander.bastools.api.shapes; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.ByteArrayOutputStream; + +import org.junit.Test; + +public class ShapesTest { + /** + * This shape is taken from the Applesoft BASIC Programmer's Reference Manual (1987), p146. + */ + public ShapeTable readStandardShapeTable() { + final byte[] sample = { 0x01, 0x00, 0x04, 0x00, 0x12, 0x3F, 0x20, 0x64, 0x2d, 0x15, 0x36, 0x1e, 0x07, 0x00 }; + ShapeTable st = ShapeTable.read(sample); + assertNotNull(st); + assertNotNull(st.shapes); + assertEquals(1, st.shapes.size()); + return st; + } + + @Test + public void testStandardShapeTableVectors() { + ShapeTable st = readStandardShapeTable(); + + VectorShape expected = new VectorShape() + .moveDown().moveDown() + .plotLeft().plotLeft() + .moveUp().plotUp().plotUp().plotUp() + .moveRight().plotRight().plotRight().plotRight() + .moveDown().plotDown().plotDown().plotDown() + .moveLeft().plotLeft(); + + Shape s = st.shapes.get(0); + assertNotNull(s); + assertEquals(expected.vectors, s.toVector().vectors); + } + + @Test + public void testStandardShapeTableBitmap() { + ShapeTable st = readStandardShapeTable(); + + BitmapShape expected = new BitmapShape(5, 5); + for (int i=1; i<=3; i++) { + expected.plot(i, 0); + expected.plot(i, 4); + expected.plot(0, i); + expected.plot(4, i); + } + + Shape s = st.shapes.get(0); + assertEquals(expected.grid, s.toBitmap().grid); + } + + @Test + public void testTextShapeExporterNoBorder() { + ShapeTable st = readStandardShapeTable(); + + final String expected = ".XXX.\n" + + "X...X\n" + + "X.+.X\n" + + "X...X\n" + + ".XXX.\n"; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ShapeExporter exp = ShapeExporter.text().noBorder().build(); + exp.export(st.shapes.get(0), outputStream); + String actual = new String(outputStream.toByteArray()); + + assertEquals(expected, actual); + } + + @Test + public void testTextShapeExporterAsciiBorder() { + ShapeTable st = readStandardShapeTable(); + + final String expected = "+-----+\n" + + "|.XXX.|\n" + + "|X...X|\n" + + "|X.+.X|\n" + + "|X...X|\n" + + "|.XXX.|\n" + + "+-----+\n"; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ShapeExporter exp = ShapeExporter.text().asciiTextBorder().build(); + exp.export(st.shapes.get(0), outputStream); + String actual = new String(outputStream.toByteArray()); + + assertEquals(expected, actual); + } + + @Test + public void testTextShapeTableExporterNoBorder() { + ShapeTable st = readStandardShapeTable(); + + // Simulate 4 of these identical shapes by adding 3 more + st.shapes.add(st.shapes.get(0)); + st.shapes.add(st.shapes.get(0)); + st.shapes.add(st.shapes.get(0)); + + final String oneExpectedRow = ".XXX. .XXX.\n" + + "X...X X...X\n" + + "X.+.X X.+.X\n" + + "X...X X...X\n" + + ".XXX. .XXX.\n"; + String expected = oneExpectedRow + "\n" + oneExpectedRow; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ShapeExporter exp = ShapeExporter.text().maxWidth(12).noBorder().build(); + exp.export(st, outputStream); + String actual = new String(outputStream.toByteArray()); + + assertEquals(expected, actual); + } + + @Test + public void testTextShapeTableExporterAsciiBorder() { + ShapeTable st = readStandardShapeTable(); + + // Simulate 4 of these identical shapes by adding 3 more + st.shapes.add(st.shapes.get(0)); + st.shapes.add(st.shapes.get(0)); + st.shapes.add(st.shapes.get(0)); + + final String divider = "+-----+-----+\n"; + final String oneExpectedRow = divider + + "|.XXX.|.XXX.|\n" + + "|X...X|X...X|\n" + + "|X.+.X|X.+.X|\n" + + "|X...X|X...X|\n" + + "|.XXX.|.XXX.|\n"; + String expected = oneExpectedRow + oneExpectedRow + divider; + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ShapeExporter exp = ShapeExporter.text().maxWidth(12).asciiTextBorder().build(); + exp.export(st, outputStream); + String actual = new String(outputStream.toByteArray()); + + assertEquals(expected, actual); + } +} diff --git a/api/src/test/java/io/github/applecommander/bastools/api/shapes/VectorCommandTest.java b/api/src/test/java/io/github/applecommander/bastools/api/shapes/VectorCommandTest.java new file mode 100644 index 0000000..be48561 --- /dev/null +++ b/api/src/test/java/io/github/applecommander/bastools/api/shapes/VectorCommandTest.java @@ -0,0 +1,32 @@ +package io.github.applecommander.bastools.api.shapes; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class VectorCommandTest { + @Test + public void testDirections() { + test(0, 1, VectorCommand.MOVE_DOWN, VectorCommand.PLOT_DOWN); + test(0, -1, VectorCommand.MOVE_UP, VectorCommand.PLOT_UP); + test(-1, 0, VectorCommand.MOVE_LEFT, VectorCommand.PLOT_LEFT); + test(1, 0, VectorCommand.MOVE_RIGHT, VectorCommand.PLOT_RIGHT); + } + public void test(int xmove, int ymove, VectorCommand... commands) { + for (VectorCommand command : commands) { + assertEquals(xmove, command.xmove); + assertEquals(ymove, command.ymove); + } + } + + @Test + public void testPlot() { + test(false, VectorCommand.MOVE_DOWN, VectorCommand.MOVE_LEFT, VectorCommand.MOVE_RIGHT, VectorCommand.MOVE_UP); + test(true, VectorCommand.PLOT_DOWN, VectorCommand.PLOT_LEFT, VectorCommand.PLOT_RIGHT, VectorCommand.PLOT_UP); + } + public void test(boolean plot, VectorCommand... commands) { + for (VectorCommand command : commands) { + assertEquals(plot, command.plot); + } + } +} diff --git a/settings.gradle b/settings.gradle index 7fe5f6a..3886e9a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,9 @@ include 'api' include 'tools:bt' +include 'tools:st' rootProject.name = 'bastools' project(":api").name = 'bastools-api' project(":tools").name = 'bastools-tools' project(":tools:bt").name = 'bastools-tools-bt' +project(":tools:st").name = 'bastools-tools-st' diff --git a/tools/st/build.gradle b/tools/st/build.gradle new file mode 100644 index 0000000..e864568 --- /dev/null +++ b/tools/st/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'org.springframework.boot' version '2.0.2.RELEASE' +} + +repositories { + jcenter() +} + +apply plugin: 'application' + +mainClassName = "io.github.applecommander.bastools.tools.st.Main" + +bootJar { + manifest { + attributes( + 'Implementation-Title': 'Shape Tools CLI', + 'Implementation-Version': "${version} (${new Date().format('yyyy-MM-dd HH:mm')})" + ) + } +} + +dependencies { + compile 'info.picocli:picocli:3.0.2' + compile 'net.sf.applecommander:applesingle-api:1.2.1' + compile project(':bastools-api') +} diff --git a/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java new file mode 100644 index 0000000..f2274f7 --- /dev/null +++ b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java @@ -0,0 +1,102 @@ +package io.github.applecommander.bastools.tools.st; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.Callable; + +import io.github.applecommander.bastools.api.shapes.Shape; +import io.github.applecommander.bastools.api.shapes.ShapeExporter; +import io.github.applecommander.bastools.api.shapes.ShapeTable; +import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter.BorderStrategy; +import picocli.CommandLine.Command; +import picocli.CommandLine.Help.Visibility; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +@Command(name = "extract", description = { "Extract shapes from shape table" }, + parameterListHeading = "%nParameters:%n", + descriptionHeading = "%n", + optionListHeading = "%nOptions:%n") +public class ExtractCommand implements Callable { + @Option(names = { "-h", "--help" }, description = "Show help for subcommand", usageHelp = true) + private boolean helpFlag; + + @Option(names = "--stdin", description = "Read from stdin") + private boolean stdinFlag; + + @Option(names = "--stdout", description = "Write to stdout") + private boolean stdoutFlag; + + @Option(names = { "-o", "--output" }, description = "Write to filename") + private String filename; + + @Option(names = "--border", description = "Set border style (none, simple, box)", showDefaultValue = Visibility.ALWAYS) + private String borderStyle = "simple"; + + @Option(names = { "-w", "--width" }, description = "Set text width", showDefaultValue = Visibility.ALWAYS) + private int textWidth = 80; + + @Option(names = "--shape", description = "Extract specific shape") + private int shapeNum = 0; + + @Parameters(arity = "0..1", description = "File to process") + private Path file; + + private BorderStrategy borderStrategy; + + @Override + public Void call() throws IOException { + validateArguments(); + + ShapeTable shapeTable = stdinFlag ? ShapeTable.read(System.in) : ShapeTable.read(file); + + ShapeExporter exporter = ShapeExporter.text() + .borderStrategy(borderStrategy) + .maxWidth(textWidth) + .build(); + + if (shapeNum > 0) { + if (shapeNum <= shapeTable.shapes.size()) { + Shape shape = shapeTable.shapes.get(shapeNum-1); + if (stdoutFlag) { + exporter.export(shape, System.out); + } else { + exporter.export(shape, Paths.get(filename)); + } + } else { + throw new IOException("Invalid shape number"); + } + } else { + if (stdoutFlag) { + exporter.export(shapeTable, System.out); + } else { + exporter.export(shapeTable, Paths.get(filename)); + } + } + + return null; + } + + private void validateArguments() throws IOException { + if (stdoutFlag && filename != null) { + throw new IOException("Please choose one of stdout or output file"); + } + if ((stdinFlag && file != null) || (!stdinFlag && file == null)) { + throw new IOException("Please select ONE of stdin or file"); + } + switch (borderStyle) { + case "box": + this.borderStrategy = BorderStrategy.BOX_DRAWING; + break; + case "simple": + this.borderStrategy = BorderStrategy.ASCII_TEXT; + break; + case "none": + this.borderStrategy = BorderStrategy.NONE; + break; + default: + throw new IOException("Please select a valid border strategy"); + } + } +} \ No newline at end of file diff --git a/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/Main.java b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/Main.java new file mode 100644 index 0000000..d99bba5 --- /dev/null +++ b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/Main.java @@ -0,0 +1,48 @@ +package io.github.applecommander.bastools.tools.st; + +import java.util.Optional; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.HelpCommand; +import picocli.CommandLine.Option; + +/** + * Primary entry point into the Shape Tools utility. + */ +@Command(name = "st", mixinStandardHelpOptions = true, versionProvider = VersionProvider.class, + descriptionHeading = "%n", + commandListHeading = "%nCommands:%n", + optionListHeading = "%nOptions:%n", + description = "Shape Tools utility", + subcommands = { + ExtractCommand.class, + HelpCommand.class, + }) +public class Main implements Runnable { + @Option(names = "--debug", description = "Dump full stack trackes if an error occurs") + private static boolean debugFlag; + + public static void main(String[] args) { + try { + CommandLine.run(new Main(), args); + } catch (Throwable t) { + if (Main.debugFlag) { + t.printStackTrace(System.err); + } else { + String message = t.getMessage(); + while (t != null) { + message = t.getMessage(); + t = t.getCause(); + } + System.err.printf("Error: %s\n", Optional.ofNullable(message).orElse("An error occurred.")); + } + System.exit(1); + } + } + + @Override + public void run() { + CommandLine.usage(this, System.out); + } +} diff --git a/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/VersionProvider.java b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/VersionProvider.java new file mode 100644 index 0000000..70c57c6 --- /dev/null +++ b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/VersionProvider.java @@ -0,0 +1,17 @@ +package io.github.applecommander.bastools.tools.st; + +import io.github.applecommander.applesingle.AppleSingle; +import io.github.applecommander.bastools.api.BasTools; +import picocli.CommandLine.IVersionProvider; + +/** Display version information. Note that this is dependent on the Spring Boot Gradle plugin configuration. */ +public class VersionProvider implements IVersionProvider { + public String[] getVersion() { + return new String[] { + String.format("%s: %s", Main.class.getPackage().getImplementationTitle(), + Main.class.getPackage().getImplementationVersion()), + String.format("%s: %s", BasTools.TITLE, BasTools.VERSION), + String.format("AppleSingle API: %s", AppleSingle.VERSION) + }; + } +} \ No newline at end of file