diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/App.java b/src/main/java/de/tudresden/inf/mci/brailleplot/App.java index 611d592cde5ad56719276216af01c50fb23b9093..5dc926d11479be060e9a084c69ef35cfb00eaa46 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/App.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/App.java @@ -4,6 +4,7 @@ import de.tudresden.inf.mci.brailleplot.configparser.Format; import de.tudresden.inf.mci.brailleplot.configparser.JavaPropertiesConfigurationParser; import de.tudresden.inf.mci.brailleplot.configparser.Printer; +import de.tudresden.inf.mci.brailleplot.diagrams.CategoricalBarChart; import de.tudresden.inf.mci.brailleplot.layout.PlotCanvas; import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; import de.tudresden.inf.mci.brailleplot.point.Point2DValued; @@ -23,7 +24,6 @@ import de.tudresden.inf.mci.brailleplot.csvparser.CsvParser; import de.tudresden.inf.mci.brailleplot.csvparser.CsvType; import de.tudresden.inf.mci.brailleplot.datacontainers.CategoricalPointListContainer; import de.tudresden.inf.mci.brailleplot.datacontainers.PointList; -import de.tudresden.inf.mci.brailleplot.diagrams.BarChart; import de.tudresden.inf.mci.brailleplot.rendering.MasterRenderer; import de.tudresden.inf.mci.brailleplot.svgexporter.BoolFloatingPointDataSvgExporter; import de.tudresden.inf.mci.brailleplot.svgexporter.BoolMatrixDataSvgExporter; @@ -174,7 +174,7 @@ public final class App { CsvParser csvParser = new CsvParser(csvReader, ',', '\"'); CategoricalPointListContainer<PointList> container = csvParser.parse(CsvType.X_ALIGNED_CATEGORIES, CsvOrientation.VERTICAL); mLogger.debug("Internal data representation:\n {}", container.toString()); - BarChart barChart = new BarChart(container); + CategoricalBarChart barChart = new CategoricalBarChart(container); // Render diagram MasterRenderer renderer = new MasterRenderer(indexV4Printer, a4Format); diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimpleCategoricalPointListContainerImpl.java b/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimpleCategoricalPointListContainerImpl.java index 834e3b3a86639041a684c6d1ae8f1aea82147dfc..a486b3e013a5f1f57696e704afbfbc28d87adb6b 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimpleCategoricalPointListContainerImpl.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimpleCategoricalPointListContainerImpl.java @@ -7,8 +7,8 @@ import java.util.Objects; /** * Low effort implementation of {@link CategoricalPointListContainer}{@literal <}{@link PointList}{@literal >}. - * @author Georg Graßnick - * @version 2019.08.02 + * @author Georg Graßnick, Leonard Kupper + * @version 2019.08.29 */ public class SimpleCategoricalPointListContainerImpl extends SimplePointListContainerImpl implements CategoricalPointListContainer<PointList> { @@ -27,6 +27,12 @@ public class SimpleCategoricalPointListContainerImpl extends SimplePointListCont mCategories = new ArrayList<>(Objects.requireNonNull(initialCategories)); } + public SimpleCategoricalPointListContainerImpl(final PointListContainer<PointList> pointListContainer) { + super(pointListContainer); + mCategories = new ArrayList<>(); + mCategories.add(""); + } + @Override public int pushBackCategory(final String category) { Objects.requireNonNull(category); diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimplePointListContainerImpl.java b/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimplePointListContainerImpl.java index 028c5c192ed5a4fa61914c7d7e17d14bdab26106..34b12123f7d8c1e03bdec1e41136e4e2c9e04171 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimplePointListContainerImpl.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/datacontainers/SimplePointListContainerImpl.java @@ -3,11 +3,12 @@ package de.tudresden.inf.mci.brailleplot.datacontainers; import java.util.LinkedList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; /** * A low effort implementation of {@link PointListContainer}{@literal <}{@link PointList}{@literal >}. - * @author Georg Graßnick - * @version 2019.07.29 + * @author Georg Graßnick, Leonard Kupper + * @version 2019.08.29 */ public class SimplePointListContainerImpl extends AbstractPointContainer<PointList> implements PointListContainer<PointList> { @@ -19,4 +20,10 @@ public class SimplePointListContainerImpl extends AbstractPointContainer<PointLi Objects.requireNonNull(initialElements); mElements = new LinkedList<>(initialElements); } + + public SimplePointListContainerImpl(final PointListContainer<PointList> pointListContainer) { + Objects.requireNonNull(pointListContainer); + mElements = new LinkedList<>(pointListContainer.stream().collect(Collectors.toList())); + calculateExtrema(); + } } diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/BarChart.java b/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/BarChart.java index f78bb04d913e27b5f20c04c6aaa4df66b9d18bec..ee769a1b808fca4bdc28854241eb865fbc7d40dc 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/BarChart.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/BarChart.java @@ -13,7 +13,7 @@ import java.util.stream.Collectors; /** * Representation of a bar chart with basic data functions. Implements Renderable. * @author Richard Schmidt, Georg Graßnick - * @version 2019.07.29 + * @version 2019.09.02 */ public class BarChart implements Renderable { private PointListContainer<PointList> mData; @@ -23,15 +23,6 @@ public class BarChart implements Renderable { mData = data; } - /** - * Getter for the total number of categories. - * - * @return int number of categories - */ - public int getCategoryCount() { - return mData.getSize(); - } - /** * Getter for the category names in a list. * diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/CategoricalBarChart.java b/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/CategoricalBarChart.java new file mode 100644 index 0000000000000000000000000000000000000000..abd62ee9c5be3016c6a1084c879f2ad854c0738f --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/diagrams/CategoricalBarChart.java @@ -0,0 +1,42 @@ +package de.tudresden.inf.mci.brailleplot.diagrams; + +import de.tudresden.inf.mci.brailleplot.datacontainers.CategoricalPointListContainer; +import de.tudresden.inf.mci.brailleplot.datacontainers.PointList; + +/** + * Representation of a bar chart composed from categories of multiple bars each. Implements Renderable. + * @author Leonard Kupper + * @version 2019.09.02 + */ +public class CategoricalBarChart extends BarChart { + + private CategoricalPointListContainer<PointList> mData; + + public CategoricalBarChart(final CategoricalPointListContainer<PointList> data) { + /* + This constructor is supposed to create a bar chart with categories. Since it is just an extension of + the normal BarChart, the super() constructor is called first. The mData member is then set to "hide" the + parents PointListContainer member. + (Because the reference to the data of a CategoricalBarChart must be a CategoricalPointListContainer instead.) + */ + super(data); + mData = data; + } + + /** + * Gets the name of the category at the specified index. + * @param index The index of the category. + * @return The name of the category. + */ + public String getCategoryName(final int index) { + return mData.getCategory(index); + } + + /** + * Gets the total number of categories. + * @return Number of categories as int. + */ + public int getNumberOfCategories() { + return mData.getNumberOfCategories(); + } +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/layout/RasterCanvas.java b/src/main/java/de/tudresden/inf/mci/brailleplot/layout/RasterCanvas.java index 6878b795c98e829d07d702e082b27e5f7d785bc4..1d268a577595f878cf2865da11eaa6e8ac754e89 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/layout/RasterCanvas.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/layout/RasterCanvas.java @@ -2,6 +2,7 @@ package de.tudresden.inf.mci.brailleplot.layout; import de.tudresden.inf.mci.brailleplot.configparser.Format; import de.tudresden.inf.mci.brailleplot.configparser.Printer; +import de.tudresden.inf.mci.brailleplot.printabledata.BrailleCell6; import de.tudresden.inf.mci.brailleplot.printabledata.MatrixData; import de.tudresden.inf.mci.brailleplot.printabledata.SimpleMatrixDataImpl; import org.slf4j.Logger; @@ -203,6 +204,10 @@ public class RasterCanvas extends AbstractCanvas<MatrixData<Boolean>> { return mCellHeight; } + public final boolean isSixDotBrailleRaster() { + return ((mCellWidth == BrailleCell6.COLUMN_COUNT) && (mCellHeight == BrailleCell6.ROW_COUNT)); + } + public final double getHorizontalDotDistance() { return mHorizontalDotDistance; } diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/printerbackend/PrintDirector.java b/src/main/java/de/tudresden/inf/mci/brailleplot/printerbackend/PrintDirector.java index 501450bfef4078c97f60741b6bda2180c3c70ed1..1ac2ba4a64afa11f20d56f63b2f90ec277c5fa7d 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/printerbackend/PrintDirector.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/printerbackend/PrintDirector.java @@ -111,7 +111,6 @@ public class PrintDirector { * Private method for sending the data to the printer. Separated from the public method so that the assemble process * and the printing process are separated logically, but from outside it looks like it all happens in one method. * @param data Data to be printed. - * @throws PrintException If the printing job could not be completed. */ private void print(final byte[] data) { diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Axis.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Axis.java index 599262702397044b40ba0aedab15dde3e8e82a91..2406f03bfd54140e03172cf95387152317f4a03f 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Axis.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Axis.java @@ -8,7 +8,7 @@ import java.util.Objects; /** * The representation of a visible axis with a line, tickmarks and labels. * @author Leonard Kupper - * @version 2019.07.09 + * @version 2019.08.29 */ public class Axis implements Renderable { @@ -157,7 +157,7 @@ public class Axis implements Renderable { } /** - * Get the area of the canvas on which the area is to be drawn. + * Get the area of the canvas on which the axis is to be drawn. * @return A {@link Rectangle} representing the area. */ public Rectangle getBoundary() { @@ -165,7 +165,8 @@ public class Axis implements Renderable { } /** - * Set the area of the canvas on which the area is to be drawn. + * Set the area of the canvas on which the axis is to be drawn. + * Please note that this only limits the length of the axis in its respective orientation. * @param boundary A {@link Rectangle} representing the area. */ public void setBoundary(final Rectangle boundary) { diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BarChartRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BarChartRasterizer.java new file mode 100644 index 0000000000000000000000000000000000000000..3c1e1f611f15a4c84322f7fd7b0a6621282c8cb5 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BarChartRasterizer.java @@ -0,0 +1,393 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +import de.tudresden.inf.mci.brailleplot.datacontainers.PointList; +import de.tudresden.inf.mci.brailleplot.diagrams.BarChart; +import de.tudresden.inf.mci.brailleplot.diagrams.CategoricalBarChart; +import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaException; +import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; +import de.tudresden.inf.mci.brailleplot.layout.Rectangle; +import de.tudresden.inf.mci.brailleplot.point.Point2DDouble; +import de.tudresden.inf.mci.brailleplot.printabledata.MatrixData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static java.lang.Math.abs; +import static java.lang.Math.ceil; +import static java.lang.Math.floor; +import static java.lang.Math.log10; +import static java.lang.Math.max; +import static java.lang.Math.min; +import static java.lang.Math.pow; +import static java.lang.Math.round; + +/** + * A rasterizer for instances of {@link CategoricalBarChart} which is able to display multiple bars per category. + * The rasterizer is 'cell' based, restricted to 6-dot layouts. + * @author Leonard Kupper + * @version 2019.09.02 + */ +public class BarChartRasterizer implements Rasterizer<CategoricalBarChart> { + + // Constants + private static final int X_AXIS_UNIT_SIZE_DOTS = 4; // the size of one step on the x-axis in dots. Equals the amount of dots after which the used bar textures will repeat + private static final int X_AXIS_TICK_SIZE_DOTS = 2; // the size of the x-axis tickmarks in dots + private static final int HELP_LINE_MIN_LENGTH_DOTS = 5; // the min. distance in dots for which a help line from group caption to the groups first bar will appear + private static final int LEGEND_TEXTURE_BAR_LENGTH = 3; // tells how big (in axis units) texture example bars in the legend are. + + private static final int DECIMAL_BASE = 10; // the base to which the magnitude of the axis scale will be calculated. change if you love headaches. + + // Rasterizer Properties + private int mMaximumTitleHeightCells; // max. height of title area in cells + private int mCaptionLengthCells; // width of the space reserved for category captions in cells + private int mXAxisHeightCells; // height of the x-axis including ticks and labels in cells + private double[] mUnitScalings; // valid multiples of order of magnitude (x value range) for the scaling of one x-axis step + private int mMaxBarThicknessCells; // the maximum thickness of a single bar in cells + private int mMinBarThicknessCells; // the minimum thickness of a single bar in cells + private int mTitlePaddingCells; // the padding size between the title line(s) and the chart in cells + private int mCaptionPaddingCells; // the padding size between the group captions on the left and the bar groups in cells + private int mGroupPaddingCells; // the padding size between two neighbouring bar groups in cells + private int mBarPaddingCells; // the padding size between two bars inside the same group in cells + + // Texture Management + private List<Texture<Boolean>> mTextures = new ArrayList<>(); // A list of textures to differentiate bars inside a group + // The alignment offsets for each texture ... + private List<Integer> mPositiveTextureAlignments = new ArrayList<>(); // ... when used on a bar representing a positive value + private List<Integer> mNegativeTextureAlignments = new ArrayList<>(); // ... or a negative value (it matters) + + // Sub rasterizers + private BrailleTextRasterizer mTextRasterizer = new BrailleTextRasterizer(); + private LinearMappingAxisRasterizer mAxisRasterizer = new LinearMappingAxisRasterizer(); + private TextureRasterizer mTextureRasterizer = new TextureRasterizer(); + private LegendRasterizer mLegendRasterizer = new LegendRasterizer(); + + // Intermediate variables + // These will be calculated throughout the process and must be available between different functions + private int mBarThickness; + private Rectangle mFullChartCellArea, mPositiveChartCellArea, mNegativeChartCellArea, mCaptionCellArea; + + /** + * Initialization of algorithm parameters. + */ + private void initConfig() { + + // This method will currently set the properties to some default values, + // but we could change it to load them from somewhere else. Please comment in review. + + final double tenth = 0.1, fifth = 0.2, quarter = 0.25, half = 0.5; + mUnitScalings = new double[]{tenth, fifth, quarter, half, 1.0}; + + mMaximumTitleHeightCells = 2; + mCaptionLengthCells = 1; + mXAxisHeightCells = 2; + + final int defaultMaxBarThicknessCells = 3; + mMaxBarThicknessCells = defaultMaxBarThicknessCells; + mMinBarThicknessCells = 1; + + mTitlePaddingCells = 0; + mCaptionPaddingCells = 2; + mGroupPaddingCells = 2; + mBarPaddingCells = 1; + + // Load textures + double[] rotate90 = {0, 0, 0, 1, 1, 0}; + registerTexture(new Texture<>(TexturedArea.BOTTOM_T_PATTERN).applyAffineTransformation(rotate90), 0, 0); + registerTexture(new Texture<>(TexturedArea.LINE_PATTERN).applyAffineTransformation(rotate90), 0, 0); + registerTexture(new Texture<>(TexturedArea.LETTER_Y_PATTERN).applyAffineTransformation(rotate90), 1, 1); + registerTexture(new Texture<>(TexturedArea.DASHED_PATTERN).applyAffineTransformation(rotate90), 0, 2); + registerTexture(new Texture<>(TexturedArea.GRID_PATTERN).applyAffineTransformation(rotate90), 1, 1); + } + + private int registerTexture(final Texture<Boolean> texture, final int posAlign, final int negAlign) { + mTextures.add(texture); + mPositiveTextureAlignments.add(posAlign); + mNegativeTextureAlignments.add(negAlign); + return mTextures.size(); + } + + /** + * Rasterizes a {@link BarChart} instance onto a {@link RasterCanvas}. + * @param diagram A instance of {@link BarChart} representing the bar chart diagram. + * @param canvas A instance of {@link RasterCanvas} representing the target for the rasterizer output. + * @throws InsufficientRenderingAreaException If too few space is available on the {@link RasterCanvas} + * to display the given diagram. + */ + public void rasterize(final CategoricalBarChart diagram, final RasterCanvas canvas) throws InsufficientRenderingAreaException { + + if (!canvas.isSixDotBrailleRaster()) { + throw new InsufficientRenderingAreaException("This rasterizer can only work with a 6-dot braille grid."); + } + + initConfig(); + drawDiagram(diagram, canvas); + } + + private void drawDiagram(final CategoricalBarChart diagram, final RasterCanvas canvas) throws InsufficientRenderingAreaException { + try { + Rectangle referenceCellArea = canvas.getCellRectangle(); + + // PHASE 1 - LAYOUT: The following calculations will divide the canvas area to create the basic chart layout. + // Diagram Title + String title = "Arbeitslosenzahl in Deutschland"; // TODO: get title from diagram + int titleLength = mTextRasterizer.getBrailleStringLength(title); + int titleHeight = (int) Math.ceil(titleLength / referenceCellArea.getWidth()); + if (titleHeight > mMaximumTitleHeightCells) { + throw new InsufficientRenderingAreaException("Title is too long. (Exceeds maximum height)"); + } + Rectangle titleDotArea = canvas.toDotRectangle(referenceCellArea.removeFromTop(titleHeight)); // TODO: BrailleTextRasterizer will take cell rectangle later + // Y-Axis Name + referenceCellArea.removeFromTop(mTitlePaddingCells); + Rectangle yAxisNameDotArea = canvas.toDotRectangle(referenceCellArea.removeFromTop(1)); + // X-Axis Name + Rectangle xAxisNameDotArea = canvas.toDotRectangle(referenceCellArea.removeFromBottom(1)); + // X-Axis Area & Origin Y Coordinate + Rectangle xAxisDotArea = canvas.toDotRectangle(referenceCellArea.removeFromBottom(mXAxisHeightCells)); + int originYDotCoordinate = xAxisDotArea.intWrapper().getY(); // top edge below chart area + // Save full space between title and X-Axis for later + mFullChartCellArea = new Rectangle(referenceCellArea); + // Caption Area + mCaptionCellArea = referenceCellArea.removeFromLeft(mCaptionLengthCells); + referenceCellArea.removeFromLeft(mCaptionPaddingCells); + // X-Axis Scaling + double negativeValueRangeSize = findNegativeValueRangeSize(diagram); + double positiveValueRangeSize = findPositiveValueRangeSize(diagram); + double valueRangeSize = negativeValueRangeSize + positiveValueRangeSize; + referenceCellArea.removeFromLeft(1); // Reserve for Y-Axis + int availableUnits = findAvailableUnits(referenceCellArea, canvas); + double xAxisScaling = findAxisScaling(valueRangeSize, availableUnits); + double xAxisScalingMagnitude = pow(DECIMAL_BASE, floor(log10(xAxisScaling))); + // Bar Area with positive and negative partition + int negativeAvailableUnits = (int) round((negativeValueRangeSize / valueRangeSize) * availableUnits); + int positiveAvailableUnits = availableUnits - negativeAvailableUnits; + int negativeCells = (negativeAvailableUnits * X_AXIS_UNIT_SIZE_DOTS) / canvas.getCellWidth(); + int positiveCells = (positiveAvailableUnits * X_AXIS_UNIT_SIZE_DOTS) / canvas.getCellWidth(); + mNegativeChartCellArea = referenceCellArea.removeFromLeft(negativeCells); + mPositiveChartCellArea = referenceCellArea.removeFromLeft(positiveCells); + // Re-append the reserved space for Y-Axis to the left. + mNegativeChartCellArea.setX(mNegativeChartCellArea.getX() - 1); + mNegativeChartCellArea.setWidth(mNegativeChartCellArea.getWidth() + 1); + // Y-Axis Area & Origin X Coordinate + Rectangle yAxisDotArea; + yAxisDotArea = canvas.toDotRectangle(mNegativeChartCellArea).fromRight(1); + int originXDotCoordinate = yAxisDotArea.intWrapper().getX(); // right edge between negative and positive chart area + // Bar Groups (Categories) + int amountOfGroups = diagram.getDataSet().getSize(); // Count the total amount of groups + int amountOfBars = countTotalBarAmount(diagram); // and bars + if (amountOfGroups == amountOfBars) { // If each group only contains a single bar + mGroupPaddingCells = mBarPaddingCells; // this is done because there are no 'real' groups. + } + int availableSizeCells = mFullChartCellArea.intWrapper().getHeight(); + int baseSizeCells = baseSize(amountOfGroups, mGroupPaddingCells, amountOfBars, mBarPaddingCells); + mBarThickness = Math.min((int) floor((availableSizeCells - baseSizeCells) / amountOfBars), mMaxBarThicknessCells); + if (mBarThickness < mMinBarThicknessCells) { + throw new InsufficientRenderingAreaException("Not enough space to rasterize all bar groups."); + } + // Remove space from top which is not needed + int requiredSizeCells = baseSizeCells + amountOfBars * mBarThickness; + mFullChartCellArea.removeFromTop(mFullChartCellArea.intWrapper().getHeight() - requiredSizeCells); + + // PHASE 2 - RASTERIZING: Now, every element of the chart will be drawn onto the according area. + // Diagram Title + mTextRasterizer.rasterize(new BrailleText(title, titleDotArea), canvas); // TODO: use title variable + // Y-Axis: no units, no tickmarks + Axis yAxis = new Axis(Axis.Type.Y_AXIS, originXDotCoordinate, originYDotCoordinate, 1, 0); + yAxis.setBoundary(yAxisDotArea); + mAxisRasterizer.rasterize(yAxis, canvas); + // Y-Axis name + mTextRasterizer.rasterize(new BrailleText("Y-Achse Beschriftung", yAxisNameDotArea), canvas); // TODO: use axis name variable + // X-Axis: units and labels + Axis xAxis = new Axis(Axis.Type.X_AXIS, originXDotCoordinate, originYDotCoordinate, X_AXIS_UNIT_SIZE_DOTS, X_AXIS_TICK_SIZE_DOTS); + xAxis.setBoundary(xAxisDotArea); + xAxis.setLabels(generateNumericAxisLabels(xAxisScaling, xAxisScalingMagnitude, negativeAvailableUnits, positiveAvailableUnits)); + mAxisRasterizer.rasterize(xAxis, canvas); + // X-Axis name + mTextRasterizer.rasterize(new BrailleText("Ich bin die X-Achse", xAxisNameDotArea), canvas); // TODO: use axis name variable + // The actual groups and bars: + // This is done by iterating through the diagram data set and drawing borders with the respective padding based on whether switched + // from one bar to another or a group to another. In between, the bars are rasterized as textured areas, with a line on the bars top. + Rectangle borderBeforeCellArea, barCellArea, borderAfterCellArea; + Map<String, String> groupNameExplanations = new LinkedHashMap<>(); + Map<Texture<Boolean>, String> textureExplanations = new LinkedHashMap<>(); + // Reserve first line for first border. + borderBeforeCellArea = mFullChartCellArea.removeFromTop(1); + char groupCaptionLetter = 'a'; + int group = 0; + for (PointList pointList : diagram.getDataSet()) { // For each group: + String groupName = pointList.getName(); // Save group name for legend + groupNameExplanations.put(Character.toString(groupCaptionLetter), groupName); + int bar = 0, amountOfBarsInGroup = pointList.getSize(); + for (Point2DDouble point : pointList) { // For each bar in group: + int barLength = (int) round(X_AXIS_UNIT_SIZE_DOTS * point.getY() / xAxisScaling); + barCellArea = mFullChartCellArea.removeFromTop(mBarThickness); + if (bar < (amountOfBarsInGroup - 1)) { + borderAfterCellArea = mFullChartCellArea.removeFromTop(mBarPaddingCells); // If another bar in the same group follows, use bar padding. + } else if (group < (amountOfGroups - 1)) { + borderAfterCellArea = mFullChartCellArea.removeFromTop(mGroupPaddingCells); // If a new group follows, use group padding. + } else { + borderAfterCellArea = mFullChartCellArea.removeFromTop(1); // If nothing follows, use single line for last border. + } + // the actual bar + int textureID = bar % mTextures.size(); + drawBar(barLength, originXDotCoordinate, borderBeforeCellArea, barCellArea, borderAfterCellArea, textureID, canvas); + borderBeforeCellArea = borderAfterCellArea; + // Save used texture and corresponding name for explanation in legend + Texture<Boolean> exampleTexture = mTextures.get(textureID).setAffineTransformation(new double[]{mPositiveTextureAlignments.get(textureID), 0}); + textureExplanations.put(exampleTexture, diagram.getCategoryName(bar)); + // the group caption for each first bar in a group + if (bar == 0) { + Rectangle groupCaptionCellArea = canvas.toDotRectangle(mCaptionCellArea.intersectedWith(barCellArea)); + BrailleText captionName = new BrailleText(Character.toString(groupCaptionLetter), groupCaptionCellArea); + mTextRasterizer.rasterize(captionName, canvas); + groupCaptionLetter++; + int dashedLineStartX = canvas.toDotRectangle(mNegativeChartCellArea).intWrapper().getX(); // dashed caption help line + int dashedLineStartY = canvas.toDotRectangle(barCellArea).intWrapper().getY() + 1; + int dashedLineLength = max(0, (originXDotCoordinate - 1 + min(0, barLength)) - dashedLineStartX - 1); + if (dashedLineLength >= HELP_LINE_MIN_LENGTH_DOTS) { // omit short help lines + Rasterizer.dashedLine(dashedLineStartX, dashedLineStartY, dashedLineLength, true, canvas.getCurrentPage(), 1); + } + } + bar++; + } + group++; + } + + // PHASE 3 - DIAGRAM LEGEND: Symbols and textures are explained in the legend which will be created by the LegendRasterizer + Legend diagramLegend = new Legend(title); // Create a legend container + diagramLegend.addSymbolExplanation("Achsenskalierung:", "X-Achse", "Größenordung " + xAxisScalingMagnitude); // Explain axis scaling + diagramLegend.addSymbolExplanationGroup("Kategorien:", groupNameExplanations); // Explain bar group single character captions + if (textureExplanations.size() > 1) { // Explain textures (if multiple of them were used) + diagramLegend.addTextureExplanationGroup("Reihen:", textureExplanations); + diagramLegend.setTextureExampleSize(X_AXIS_UNIT_SIZE_DOTS * LEGEND_TEXTURE_BAR_LENGTH / canvas.getCellWidth(), mBarThickness); + } + mLegendRasterizer.rasterize(diagramLegend, canvas); // Rasterize legend + } catch (Rectangle.OutOfSpaceException e) { + throw new InsufficientRenderingAreaException("The layout for the amount of given data can not be fitted on the format.", e); + } + } + + private void drawBar( + final int barLength, + final int originXDotCoordinate, + final Rectangle topBorderCellArea, + final Rectangle barTextureCellArea, + final Rectangle bottomBorderCellArea, + final int textureID, + final RasterCanvas canvas + ) throws Rectangle.OutOfSpaceException, InsufficientRenderingAreaException { + + MatrixData<Boolean> page = canvas.getCurrentPage(); + + // First draw the top border of the top bar + int topBorderYDotCoordinate = canvas.toDotRectangle(topBorderCellArea).intWrapper().getBottom(); + Rasterizer.line(originXDotCoordinate, topBorderYDotCoordinate, barLength, true, page, true); + + // Then draw the texture and line at bars end + Rectangle barTextureDotArea, endBorderLine; + int textureAlignment; + if (barLength >= 0) { + textureAlignment = mPositiveTextureAlignments.get(textureID); + barTextureDotArea = canvas.toDotRectangle(barTextureCellArea + .intersectedWith(mPositiveChartCellArea)) + .fromLeft(barLength); + endBorderLine = barTextureDotArea.removeFromRight(min(barLength, 1)); // min required for the case barLength=0 + } else { + barTextureDotArea = canvas.toDotRectangle(barTextureCellArea + .intersectedWith(mNegativeChartCellArea)) + .fromRight(abs(barLength)).translatedBy(-1, 0); + endBorderLine = barTextureDotArea.removeFromLeft(1); + textureAlignment = mNegativeTextureAlignments.get(textureID) + barLength % X_AXIS_UNIT_SIZE_DOTS; + } + if (barLength != 0) { + Rasterizer.rectangle(endBorderLine, page, true); + mTextureRasterizer.rasterize( + new TexturedArea( + mTextures.get(textureID).setAffineTransformation(new double[]{textureAlignment, 0}), + barTextureDotArea), + canvas + ); + } + + // Lastly draw the bottom border of the top bar + int bottomBorderYDotCoordinate = canvas.toDotRectangle(bottomBorderCellArea).intWrapper().getY(); + Rasterizer.line(originXDotCoordinate, bottomBorderYDotCoordinate, barLength, true, page, true); + } + + // HELP METHODS + // ============ + + private double findNegativeValueRangeSize(final BarChart diagram) { + return abs(min(diagram.getMinY(), 0)); + } + private double findPositiveValueRangeSize(final BarChart diagram) { + return max(diagram.getMaxY(), 0); + } + + private int findAvailableUnits(final Rectangle cellArea, final RasterCanvas canvas) { + int availableCells = cellArea.intWrapper().getWidth(); + double cellsPerXAxisUnit = ((double) X_AXIS_UNIT_SIZE_DOTS / canvas.getCellWidth()); + return (int) Math.floor(availableCells / cellsPerXAxisUnit); + } + + private double findAxisScaling(final double valueRangeSize, final int availableUnits) { + double minRangePerUnit = valueRangeSize / availableUnits; // this range must fit into one 'axis step' + double orderOfMagnitude = pow(DECIMAL_BASE, ceil(log10(minRangePerUnit))); + double scaledRangePerUnit = 0; + for (double scaling : mUnitScalings) { + scaledRangePerUnit = (scaling * orderOfMagnitude); + if (scaledRangePerUnit >= minRangePerUnit) { + break; + } + } + return scaledRangePerUnit; + } + + private int countTotalBarAmount(final BarChart diagram) throws InsufficientRenderingAreaException { + int amountOfBars = 0; + for (PointList group : diagram.getDataSet()) { + int barsInGroup = group.getSize(); + if (barsInGroup > mTextures.size()) { + throw new InsufficientRenderingAreaException("The maximum amount of bars in a group is " + mTextures.size()); + } + amountOfBars += barsInGroup; + } + if (amountOfBars < 1) { + throw new IllegalArgumentException("The given diagram does not contain any data."); + } + return amountOfBars; + } + + private int baseSize(final int groups, final int groupPad, final int bars, final int barPad) { + return (groups * (groupPad - barPad) + (bars * barPad) + 2 - groupPad); + } + + /* + This commented method is a different approach for labeling the axis, but I don't know if we should keep it. + The idea is to label the tickmarks with a running index like "a, b, c, ...." and explaining every + single index charater on the legend. But I didn't found any example of this being done in practice. + + private Map<Integer, String> generateSingleCharAxisLabels(final int negativeUnits, final int positiveUnits) { + Map<Integer, String> labels = new HashMap<>(); + char labelLetter = 1; + for (int axisTick = negativeUnits * -1; axisTick <= positiveUnits; axisTick += 1) { + labels.put(axisTick, Character.toString(labelLetter)); + labelLetter++; + } + return labels; + } + */ + + private Map<Integer, String> generateNumericAxisLabels(final double scaling, final double orderOfMagnitude, final int negativeUnits, final int positiveUnits) { + Map<Integer, String> labels = new HashMap<>(); + for (int axisTick = negativeUnits * -1; axisTick <= positiveUnits; axisTick += 1) { + int value = (int) ((axisTick * scaling) / orderOfMagnitude); + String label = Integer.toString(value); + labels.put(axisTick, label); + } + return labels; + } + +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BrailleTextRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BrailleTextRasterizer.java index 4c51958182cd32c3613900b1da13edbba8892006..aeda92e9142d49eb2255b19d83edee315fb00056 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BrailleTextRasterizer.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/BrailleTextRasterizer.java @@ -5,24 +5,50 @@ import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; /** * A rasterizer for text on braille grids. This class is still a stub and must be implemented! - * @version 2019.07.21 + * @version 2019.08.29 * @author Leonard Kupper */ public final class BrailleTextRasterizer implements Rasterizer<BrailleText> { + + // Ignore this class. + // It has to be replaced completely with Andreys implementation when the text rasterizer branch gets merged. + @Override + @SuppressWarnings("checkstyle:MagicNumber") public void rasterize(final BrailleText data, final RasterCanvas canvas) throws InsufficientRenderingAreaException { // TODO: rasterize the text (Take different grids into consideration! 6-dot / 8-dot) // Until then, we just display dummy characters int x = data.getArea().intWrapper().getX(); int y = data.getArea().intWrapper().getY(); for (int i = 0; i < data.getText().length(); i++) { - canvas.getCurrentPage().setValue(y, x, true); - canvas.getCurrentPage().setValue(y + 1, x + 1, true); - canvas.getCurrentPage().setValue(y + 2, x, true); - x += 2; + if (x >= canvas.getCurrentPage().getColumnCount()) { + x = data.getArea().intWrapper().getX(); + y += canvas.getCellHeight(); + } + char character = data.getText().charAt(i); + if (character != 0x20) { + boolean dot1 = ((character % 2) > 0); + boolean dot2 = ((character / 2 % 2) > 0); + boolean dot3 = ((character / 4 % 2) > 0); + boolean dot4 = ((character / 8 % 2) > 0); + boolean dot5 = ((character / 16 % 2) > 0); + boolean dot6 = ((character / 32 % 2) > 0); + canvas.getCurrentPage().setValue(y, x, dot1); + canvas.getCurrentPage().setValue(y + 1, x, dot2); + canvas.getCurrentPage().setValue(y + 2, x, dot3); + canvas.getCurrentPage().setValue(y, x + 1, dot4); + canvas.getCurrentPage().setValue(y + 1, x + 1, dot5); + canvas.getCurrentPage().setValue(y + 2, x + 1, dot6); + } + + x += canvas.getCellWidth(); } } + public int getBrailleStringLength(final String str) { + return str.length(); + } + // TODO: Completely replace with help methods to calculate suited area for left or right alignment of given text. public int calculateRequiredHeight(final String text, final int xPos, final int yPos, final int maxWidth, final RasterCanvas canvas) { diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Legend.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Legend.java new file mode 100644 index 0000000000000000000000000000000000000000..1f4b13c4d97c55a9971da6323b5e6b23c3f4f762 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Legend.java @@ -0,0 +1,122 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Simple representation of a legend. + * @author Leonard Kupper + * @version 2019.08.29 + */ +public class Legend implements Renderable { + + private String mTitle; + private Map<String, Map<String, String>> mStringExplanationLists = new LinkedHashMap<>(); + private Map<String, Map<Texture<Boolean>, String>> mTextureExplanationLists = new LinkedHashMap<>(); + + private int mTextureExampleWidthCells = 1; + private int mTextureExampleHeightCells = 1; + + /** + * Constructor. Creates a legend. + * @param title The title of the legend. + */ + public Legend(final String title) { + setTitle(title); + } + + /** + * Sets a new title. + * @param title The new title for the legend. + */ + public void setTitle(final String title) { + mTitle = Objects.requireNonNull(title); + } + + /** + * Gets the current title of the legend. + * @return A {@link String} containing the title. + */ + public String getTitle() { + return mTitle; + } + + /** + * Add a text symbol and the associated description text to the legend. + * @param groupName The name of the header under which explanations of this group will be placed. (e.g. "Categories") + * @param symbol The actual symbol to be explained, which appears in the diagram. (e.g. "A") + * @param descriptionText A text describing the meaning of the symbol/abbreviation. (e.g. a long name of a category) + */ + public void addSymbolExplanation(final String groupName, final String symbol, final String descriptionText) { + if (!mStringExplanationLists.containsKey(groupName)) { + mStringExplanationLists.put(groupName, new LinkedHashMap<>()); + } + mStringExplanationLists.get(groupName).put(symbol, descriptionText); + } + + /** + * Add a texture and the associated description text to the legend. + * @param groupName The name of the header under which explanations of this group will be placed. (e.g. "Series") + * @param texture The actual texture to be explained, which is used in the diagram. + * @param descriptionText A text describing the meaning of the texture. (e.g. "Series 1") + */ + public void addTextureExplanation(final String groupName, final Texture<Boolean> texture, final String descriptionText) { + if (!mTextureExplanationLists.containsKey(groupName)) { + mTextureExplanationLists.put(groupName, new LinkedHashMap<>()); + } + mTextureExplanationLists.get(groupName).put(texture, descriptionText); + } + + /** + * Add a complete group of text symbol explanations to the legend. + * @param groupName The name of the header under which explanations of this group will be placed. (e.g. "Categories") + * @param explanationGroup A map listing the text symbols and their associated description texts. + */ + public void addSymbolExplanationGroup(final String groupName, final Map<String, String> explanationGroup) { + mStringExplanationLists.put(groupName, explanationGroup); + } + + /** + * Add a complete group of texture explanations to the legend. + * @param groupName The name of the header under which explanations of this group will be placed. (e.g. "Categories") + * @param explanationGroup A map listing the textures and their associated description texts. + */ + public void addTextureExplanationGroup(final String groupName, final Map<Texture<Boolean>, String> explanationGroup) { + mTextureExplanationLists.put(groupName, explanationGroup); + } + + /** + * Set the size (in cells) of the example rectangles displaying textures from the texture explanation groups. + * @param widthCells The new width in cells. + * @param heightCells The new height in cells. + */ + public void setTextureExampleSize(final int widthCells, final int heightCells) { + mTextureExampleWidthCells = widthCells; + mTextureExampleHeightCells = heightCells; + } + + /** + * Get all text symbol explanation groups from the legend. + * @return A map associating every group name with a map listing the text symbols and their associated description texts. + */ + final Map<String, Map<String, String>> getSymbolExplanationGroups() { + return mStringExplanationLists; + } + + /** + * Get all texture explanation groups from the legend. + * @return A map associating every group name with a map listing the textures and their associated description texts. + */ + final Map<String, Map<Texture<Boolean>, String>> getTextureExplanationGroups() { + return mTextureExplanationLists; + } + + final int getTextureExampleWidthCells() { + return mTextureExampleWidthCells; + } + + final int getTextureExampleHeightCells() { + return mTextureExampleHeightCells; + } +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LegendRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LegendRasterizer.java new file mode 100644 index 0000000000000000000000000000000000000000..d40b74f28bb40c6e37a978b9311a45c17df344c5 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LegendRasterizer.java @@ -0,0 +1,144 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaException; +import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; +import de.tudresden.inf.mci.brailleplot.layout.Rectangle; +import de.tudresden.inf.mci.brailleplot.printabledata.MatrixData; +import java.util.Map; +import static java.lang.Integer.max; + +/** + * A rasterizer that is able to draw a legend on a new page. + * @author Leonard Kupper + * @version 2019.08.28 + */ +public class LegendRasterizer implements Rasterizer<Legend> { + + private RasterCanvas mCanvas; + private Legend mLegend; + + private static final int MIN_TEXT_WIDTH_CELLS = 10; // how much space should be available for an explanation text at least. (To avoid excessive line breaking) + private static final int EXPLANATION_TEXT_INDENTATION_CELLS = 1; // indentation for explanation texts. + private static final String LEGEND_KEYWORD = "Legende:"; // title for the legend + + // Sub rasterizers + private BrailleTextRasterizer mTextRasterizer = new BrailleTextRasterizer(); + private TextureRasterizer mTextureRasterizer = new TextureRasterizer(); + + /** + * Rasterizes a {@link Legend} instance onto a {@link RasterCanvas}. Important: This creates a new page on the canvas! + * @param legend An instance of {@link Legend} containing the legend contents. + * @param canvas An instance of {@link RasterCanvas} representing the target for the rasterizer output. + */ + @Override + public void rasterize(final Legend legend, final RasterCanvas canvas) throws InsufficientRenderingAreaException { + + mCanvas = canvas; + mLegend = legend; + + // Create a fresh page on the canvas. + MatrixData<Boolean> page = canvas.getNewPage(); + Rectangle referenceCellArea = canvas.getCellRectangle(); + + try { + + // Write "Legend" keyword + title + writeLine(LEGEND_KEYWORD + " " + legend.getTitle(), referenceCellArea); + + // String explanation lists + for (Map.Entry<String, Map<String, String>> list : legend.getSymbolExplanationGroups().entrySet()) { + String groupName = list.getKey(); + writeLine("", referenceCellArea); // Leave space of one empty line + writeLine(groupName + ":", referenceCellArea); + moveIndentation(referenceCellArea, EXPLANATION_TEXT_INDENTATION_CELLS); // set indentation + for (Map.Entry<String, String> explanation : list.getValue().entrySet()) { + String symbol = explanation.getKey(); + String description = explanation.getValue(); + writeLine(symbol + " - " + description, referenceCellArea); + } + moveIndentation(referenceCellArea, -1 * EXPLANATION_TEXT_INDENTATION_CELLS); // reset indentation + } + + // Texture explanation lists + for (Map.Entry<String, Map<Texture<Boolean>, String>> list : legend.getTextureExplanationGroups().entrySet()) { + String groupName = list.getKey(); + writeLine("", referenceCellArea); // Leave space of one empty line + writeLine(groupName + ":", referenceCellArea); + moveIndentation(referenceCellArea, EXPLANATION_TEXT_INDENTATION_CELLS); // set indentation + for (Map.Entry<Texture<Boolean>, String> explanation : list.getValue().entrySet()) { + Texture<Boolean> texture = explanation.getKey(); + String description = explanation.getValue(); + drawTextureExample(referenceCellArea, texture, description); + } + moveIndentation(referenceCellArea, -1 * EXPLANATION_TEXT_INDENTATION_CELLS); // reset indentation + } + + } catch (Rectangle.OutOfSpaceException e) { + throw new InsufficientRenderingAreaException("The amount of data in the legend does not fit on the format.", e); + } + + } + + private void drawTextureExample( + final Rectangle referenceCellArea, + final Texture<Boolean> texture, + final String description + ) throws Rectangle.OutOfSpaceException, InsufficientRenderingAreaException { + + MatrixData<Boolean> page = mCanvas.getCurrentPage(); + + // add padding between previous content and example + referenceCellArea.removeFromTop(1); + + // reserve the overall area for the inner texture and description text + int textureExampleHeightCells = mLegend.getTextureExampleHeightCells(); + int textureExampleWidthCells = mLegend.getTextureExampleWidthCells(); + Rectangle exampleCellArea = referenceCellArea.removeFromTop(textureExampleHeightCells); + exampleCellArea.removeFromLeft(1); // space for left border + Rectangle texturedDotArea = mCanvas.toDotRectangle(exampleCellArea.removeFromLeft(textureExampleWidthCells)); + referenceCellArea.fromTop(1); // just to make sure there is enough space for the bottom border + + // create surrounding rectangle for border of textured area + Rectangle textureDotBorder = texturedDotArea.translatedBy(-1, -1); + textureDotBorder.setHeight(textureDotBorder.getHeight() + 2); + textureDotBorder.setWidth(textureDotBorder.getWidth() + 1); + + // reserve space for the texture description text + Rectangle textCellArea = exampleCellArea; + textCellArea.removeFromLeft(1); // padding between textured area and text + // check if description height will fit + int descriptionHeightCells = (int) Math.ceil(mTextRasterizer.getBrailleStringLength(description) / textCellArea.getWidth()); + if (descriptionHeightCells > textureExampleHeightCells) { + int missingLines = descriptionHeightCells - textureExampleHeightCells; + // in this case, description text needs more lines than texture, so we need to reserve the missing space + referenceCellArea.removeFromTop(missingLines); + textCellArea.setHeight(textCellArea.getHeight() + missingLines); // expand height + } + + // draw textured area + mTextureRasterizer.rasterize(new TexturedArea(texture, texturedDotArea), mCanvas); + + // draw border + Rasterizer.rectangle(textureDotBorder, mCanvas.getCurrentPage(), true); + + // draw text + mTextRasterizer.rasterize(new BrailleText(description, mCanvas.toDotRectangle(textCellArea)), mCanvas); + } + + private void moveIndentation(final Rectangle cellArea, final int indent) { + cellArea.setX(cellArea.getX() + indent); + cellArea.setWidth(cellArea.getWidth() - indent); + } + + private void writeLine(final String text, final Rectangle cellArea) throws InsufficientRenderingAreaException, Rectangle.OutOfSpaceException { + if (cellArea.getWidth() < MIN_TEXT_WIDTH_CELLS) { + throw new InsufficientRenderingAreaException("Not enough space for legend text."); + } + // write text lines + int textLength = mTextRasterizer.getBrailleStringLength(text); + int textHeight = max(1, (int) Math.ceil(textLength / cellArea.getWidth())); + Rectangle textLineDotArea = mCanvas.toDotRectangle(cellArea.removeFromTop(textHeight)); + mTextRasterizer.rasterize(new BrailleText(text, textLineDotArea), mCanvas); + } + +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LinearMappingAxisRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LinearMappingAxisRasterizer.java index 268a9d25175262dd4a8da2436802516542d93ccf..a31c630341e6d3f2fe038839be02210b3a70b42d 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LinearMappingAxisRasterizer.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/LinearMappingAxisRasterizer.java @@ -4,21 +4,26 @@ import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaExceptio import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; import de.tudresden.inf.mci.brailleplot.layout.Rectangle; import de.tudresden.inf.mci.brailleplot.printabledata.MatrixData; - import static de.tudresden.inf.mci.brailleplot.rendering.Axis.Type.X_AXIS; import static de.tudresden.inf.mci.brailleplot.rendering.Axis.Type.Y_AXIS; -import static java.lang.Math.abs; +import static java.lang.Integer.signum; /** * A rasterizer for instances of {@link Axis} which is using a simple approach by linear mapping. * @author Leonard Kupper - * @version 2019.07.20 + * @version 2019.08.29 */ public class LinearMappingAxisRasterizer implements Rasterizer<Axis> { private BrailleTextRasterizer mTextRasterizer = new BrailleTextRasterizer(); private RasterCanvas mCanvas; + private MatrixData<Boolean> mPage; + + private Axis mAxis; + private int mStepWidth, mTickSize, mOriginX, mOriginY; + private boolean mHasLabels, mTickDetermination; + private Rectangle mBound; /** * Rasterizes a {@link Axis} instance onto a {@link RasterCanvas}. @@ -31,95 +36,146 @@ public class LinearMappingAxisRasterizer implements Rasterizer<Axis> { public void rasterize(final Axis axis, final RasterCanvas canvas) throws InsufficientRenderingAreaException { mCanvas = canvas; - MatrixData<Boolean> data = mCanvas.getCurrentPage(); + mPage = mCanvas.getCurrentPage(); - int dotX, startY, endY, dotY, startX, endX; - int stepWidth = (int) axis.getStepWidth(); - int tickSize = (int) axis.getTickSize(); - boolean setTicks = (abs(tickSize) > 0); - boolean hasLabels = axis.hasLabels(); + mAxis = axis; + mOriginX = (int) axis.getOriginX(); + mOriginY = (int) axis.getOriginY(); + mStepWidth = (int) axis.getStepWidth(); + mTickSize = (int) axis.getTickSize(); + mHasLabels = axis.hasLabels(); + if (axis.hasBoundary()) { + mBound = axis.getBoundary(); + } else { + mBound = mCanvas.getDotRectangle(); + } if (axis.getType() == X_AXIS) { - Rectangle bound; - dotY = (int) axis.getOriginY(); - if (axis.hasBoundary()) { - bound = axis.getBoundary(); + drawXAxis(); + } else if (axis.getType() == Y_AXIS) { + drawYAxis(); + } else { + throw new IllegalArgumentException("Unknown axis type: " + axis.getType()); + } + + } + + private void drawXAxis() throws InsufficientRenderingAreaException { + // draw axis line + Rectangle axisLineDotArea = new Rectangle(mBound.getX(), mOriginY, mBound.getWidth(), 1); + Rasterizer.rectangle(axisLineDotArea, mPage, true); + // draw labels and ticks + int positiveUnits = (int) Math.floor((axisLineDotArea.intWrapper().getRight() - mOriginX) / mStepWidth); + int negativeUnits = (int) Math.floor((axisLineDotArea.intWrapper().getX() - mOriginX) / mStepWidth); + for (int i = 0; i <= positiveUnits; i++) { + tryToDrawLabel(i); + } + for (int i = -1; i >= negativeUnits; i--) { + tryToDrawLabel(i); + } + } + + private void drawYAxis() throws InsufficientRenderingAreaException { + // draw axis line + Rectangle axisLineDotArea = new Rectangle(mOriginX, mBound.getY(), 1, mBound.getHeight()); + Rasterizer.rectangle(axisLineDotArea, mPage, true); + // draw labels and ticks + int positiveUnits = (int) Math.floor((mOriginY - axisLineDotArea.intWrapper().getY()) / mStepWidth); + int negativeUnits = (int) Math.floor((mOriginY - axisLineDotArea.intWrapper().getBottom()) / mStepWidth); + for (int i = 0; i <= positiveUnits; i++) { + tryToDrawLabel(i); + } + for (int i = -1; i >= negativeUnits; i--) { + tryToDrawLabel(i); + } + } + + /** + * Tries to draw a label at given index and draws a tick when needed. + * @param labelIndex The index of the label which the method will try to draw. + * @return A boolean value determining whether a tick has been set. Ticks will be drawn whenever a label can be set + * at the respective positions or when no labels are defined. + */ + private boolean tryToDrawLabel(final int labelIndex) + throws InsufficientRenderingAreaException { + + Rectangle tickmarkDotArea; + int dotX, dotY, labelCellX, labelCellY; + dotX = mOriginX; + dotY = mOriginY; + + // check preconditions + if (mHasLabels && mAxis.getLabels().containsKey(labelIndex)) { + // get label text + String labelText = mAxis.getLabels().get(labelIndex); + int stringLength = mTextRasterizer.getBrailleStringLength(labelText); + + // determine area to write the label text on + int xPad, yPad; + Rectangle labelCellArea; + int labelOffset = signum(mTickSize); // is the label above or below the tickmark? + if (mAxis.getType() == X_AXIS) { + dotX += labelIndex * mStepWidth; + labelCellX = mCanvas.getCellXFromDotX(dotX); + labelCellY = mCanvas.getCellYFromDotY(dotY + mTickSize) + labelOffset; + labelCellArea = new Rectangle(labelCellX - (stringLength / 2), labelCellY, stringLength, 1); + xPad = 1; + yPad = 0; + } else if (mAxis.getType() == Y_AXIS) { + dotY -= labelIndex * mStepWidth; + labelCellX = mCanvas.getCellXFromDotX(dotX + (mTickSize + labelOffset)) + labelOffset; + labelCellY = mCanvas.getCellYFromDotY(dotY); + if (mTickSize < 0) { + labelCellArea = new Rectangle(labelCellX - (stringLength), labelCellY, stringLength, 1); + } else { + labelCellArea = new Rectangle(labelCellX, labelCellY, stringLength, 1); + } + xPad = 0; + yPad = 0; } else { - bound = mCanvas.getDotRectangle(); + return false; } - startX = bound.intWrapper().getX(); - endX = bound.intWrapper().getRight(); - Rasterizer.fill(startX, dotY, endX, dotY, data, true); - - if (setTicks) { - int i; - startY = dotY; - endY = dotY + tickSize; - i = 0; - for (dotX = (int) axis.getOriginX(); dotX <= endX; dotX += stepWidth) { - Rasterizer.fill(dotX, startY, dotX, endY, data, true); - // TODO: refactor to have labeling functionality in extra method. - if (hasLabels && axis.getLabels().containsKey(i)) { - String label = axis.getLabels().get(i); - Rectangle labelArea = new Rectangle(dotX - 1, endY + 1, stepWidth, mCanvas.getCellHeight()); - mTextRasterizer.rasterize(new BrailleText(label, labelArea), mCanvas); - } - i++; - } - i = -1; - for (dotX = (int) axis.getOriginX() - stepWidth; dotX >= startX; dotX -= stepWidth) { - Rasterizer.fill(dotX, startY, dotX, endY, data, true); - if (hasLabels && axis.getLabels().containsKey(i)) { - String label = axis.getLabels().get(i); - Rectangle labelArea = new Rectangle(dotX - 1, endY + 1, stepWidth, mCanvas.getCellHeight()); - mTextRasterizer.rasterize(new BrailleText(label, labelArea), mCanvas); - } - i--; - } + + // try to draw label (depending on available space) + if (testCellsEmpty(labelCellArea, xPad, yPad)) { + mTextRasterizer.rasterize(new BrailleText(labelText, mCanvas.toDotRectangle(labelCellArea)), mCanvas); + } else { + return false; } } - if (axis.getType() == Y_AXIS) { - Rectangle bound; - dotX = (int) axis.getOriginX(); - if (axis.hasBoundary()) { - bound = axis.getBoundary(); - } else { - bound = mCanvas.getDotRectangle(); + // draw a tickmark + if (mAxis.getType() == X_AXIS) { + tickmarkDotArea = new Rectangle(dotX, dotY, 1, Math.abs(mTickSize) + 1); + if (mTickSize < 0) { + tickmarkDotArea = tickmarkDotArea.translatedBy(0, mTickSize); } - startY = bound.intWrapper().getY(); - endY = bound.intWrapper().getBottom(); - Rasterizer.fill(dotX, startY, dotX, endY, data, true); - - if (setTicks) { - int i; - startX = dotX; - endX = dotX + tickSize; - i = 0; - for (dotY = (int) axis.getOriginY(); dotY <= endY; dotY += stepWidth) { - Rasterizer.fill(startX, dotY, endX, dotY, data, true); - /* - if (hasLabels && axis.getLabels().containsKey(i)) { - String label = axis.getLabels().get(i); - Rectangle labelArea = new Rectangle(endX + Integer.signum(tickSize), dotY, stepWidth, mCanvas.getCellHeight()); - mTextRasterizer.rasterize(new BrailleText(label, labelArea), mCanvas); - } - */ - i++; - } - i = -1; - for (dotY = (int) axis.getOriginY() - stepWidth; dotY >= startY; dotY -= stepWidth) { - Rasterizer.fill(startX, dotY, endX, dotY, data, true); - /* - if (hasLabels && axis.getLabels().containsKey(i)) { - String label = axis.getLabels().get(i); - Rectangle labelArea = new Rectangle(endX + Integer.signum(tickSize), dotY, stepWidth, mCanvas.getCellHeight()); - mTextRasterizer.rasterize(new BrailleText(label, labelArea), mCanvas); - } - */ - i--; + } else if (mAxis.getType() == Y_AXIS) { + tickmarkDotArea = new Rectangle(dotX, dotY, Math.abs(mTickSize) + 1, 1); + if (mTickSize < 0) { + tickmarkDotArea = tickmarkDotArea.translatedBy(mTickSize, 0); + } + } else { + return false; + } + Rasterizer.rectangle(tickmarkDotArea, mPage, true); + return true; + } + + private boolean testCellsEmpty(final Rectangle cellArea, final int xPad, final int yPad) { + Rectangle paddedCellArea = cellArea.translatedBy(-1 * xPad, -1 * yPad); + paddedCellArea.setWidth(paddedCellArea.getWidth() + 2 * xPad); + paddedCellArea.setHeight(paddedCellArea.getHeight() + 2 * yPad); + Rectangle testDotArea = mCanvas.toDotRectangle(paddedCellArea); + for (int x = testDotArea.intWrapper().getX(); x <= testDotArea.intWrapper().getRight(); x++) { + for (int y = testDotArea.intWrapper().getY(); y <= testDotArea.intWrapper().getBottom(); y++) { + if ((mPage.getRowCount() <= y) || (mPage.getColumnCount() <= x) + || (y < 0) || (x < 0) + || mPage.getValue(y, x)) { + return false; } } } + return true; } } diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/MasterRenderer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/MasterRenderer.java index 23fe3546fe6eaed4ffacc5c948a9bcd5b1d02e9a..7d588528d63eff2073718ca0b7561fe2630cbe66 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/MasterRenderer.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/MasterRenderer.java @@ -1,9 +1,9 @@ package de.tudresden.inf.mci.brailleplot.rendering; +import de.tudresden.inf.mci.brailleplot.diagrams.CategoricalBarChart; import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaException; import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; import de.tudresden.inf.mci.brailleplot.layout.SixDotBrailleRasterCanvas; -import de.tudresden.inf.mci.brailleplot.diagrams.BarChart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,11 +38,11 @@ public final class MasterRenderer { mLogger.trace("Instantiating default rasterizers"); // Default Algorithms: - Rasterizer<BarChart> uniformTexture = new UniformTextureBarChartRasterizer(); + Rasterizer<CategoricalBarChart> barChartRasterizer = new BarChartRasterizer(); Rasterizer<Image> linearImageMapping = new ImageRasterizer(); mLogger.trace("Registering default rasterizers"); - renderingBase.registerRasterizer(new FunctionalRasterizer<BarChart>(BarChart.class, uniformTexture)); + renderingBase.registerRasterizer(new FunctionalRasterizer<CategoricalBarChart>(CategoricalBarChart.class, barChartRasterizer)); renderingBase.registerRasterizer(new FunctionalRasterizer<Image>(Image.class, linearImageMapping)); //renderingBase.registerRasterizer(new FunctionalRasterizer<ScatterPlot>(ScatterPlot.class, ScatterPlotRasterizing::fooRasterizing)); //... diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Rasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Rasterizer.java index 286a7a4d21a1d2225c6bb69d9e8b7784bca10c00..ce6b9ce8989df9cf83df75d5562d69c186eeb6a5 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Rasterizer.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Rasterizer.java @@ -83,4 +83,63 @@ public interface Rasterizer<T extends Renderable> { fill(x2, intRect.getY(), x2, y2, data, value); fill(intRect.getX(), intRect.getY(), x2, intRect.getY(), data, value); } + + /** + * Draws an orthogonal line of specified length from given point onto the raster. + * @param xStart X coordinate of start point. + * @param yStart Y coordinate of start point. + * @param length Length of the line. + * @param orientation Pass true for vertical, false for horizontal. + * @param data The target raster data container. + * @param value The value to fill the area with. + */ + static void line(int xStart, int yStart, int length, boolean orientation, MatrixData<Boolean> data, boolean value) { + if (length != 0) { + int xEnd = xStart; + int yEnd = yStart; + //int span = (int) Math.signum(length) * max(length - 1, 0); + if (orientation) { + xEnd = xStart + length; + } else { + yEnd = yStart + length; + } + fill(xStart, yStart, xEnd, yEnd, data, value); + } + } + + /** + * Draws an orthogonal dashed line of specified length from given point onto the raster. + * @param xStart X coordinate of start point. + * @param yStart Y coordinate of start point. + * @param length Length of the line. + * @param orientation Pass true for vertical, false for horizontal. + * @param data The target raster data container. + * @param dashSize The size of the line segments. + */ + static void dashedLine(int xStart, int yStart, int length, boolean orientation, MatrixData<Boolean> data, int dashSize) { + if (dashSize <= 0) { + throw new IllegalArgumentException("Dash size cannot be zero or negative!"); + } + if (length != 0) { + boolean value = false; + int xEnd = xStart; + int yEnd = yStart; + //int span = (int) Math.signum(length) * max(length - 1, 0); + if (orientation) { + xEnd = xStart + length; + } else { + yEnd = yStart + length; + } + int i = 0; + for (int y = min(yStart, yEnd); y <= Math.max(yStart, yEnd); y++) { + for (int x = min(xStart, xEnd); x <= Math.max(xStart, xEnd); x++) { + if (i % dashSize == 0) { + value = !value; + } + data.setValue(y, x, value); + i++; + } + } + } + } } diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Texture.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Texture.java new file mode 100644 index 0000000000000000000000000000000000000000..f4f85f46ad028d3f1fc6d91c38f98ebf375e4742 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/Texture.java @@ -0,0 +1,140 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +/** + * A class representing a transformable (translation, stretching, rotation) texture made up from pixel values with arbitrary type. + * @param <T> The type of the pixel values. + * @author Leonard Kupper + * @version 2019.08.23 + */ +public class Texture<T> { + + private T[][] mTexturePattern; + private int mWidth; + private int mHeight; + private static final int TRANSLATION_SIZE = 2; + private static final int LINEARTRANS_SIZE = 4; + private static final int TRANSFORMATION_SIZE = TRANSLATION_SIZE + LINEARTRANS_SIZE; + private double[] mAffineTransformation = new double[TRANSFORMATION_SIZE]; + + /** + * Constructor. + * Creates a new texture from a two-dimensional pattern. + * @param texturePattern A two-dimensional array of type double describing the texture pattern. + * e.g. diagonal line = [[1,0] + * [0,1]] + */ + public Texture(final T[][] texturePattern) { + mWidth = 0; + mHeight = validatePatternSize(texturePattern.length); + if (mHeight < 1) { + throw new IllegalArgumentException("Given texture pattern is empty."); + } + for (int i = 0; i < mHeight; i++) { + mWidth = Math.max(mWidth, validatePatternSize(texturePattern[i].length)); + } + mTexturePattern = texturePattern; + setAffineTransformation(new double[]{0, 0, 1, 0, 0, 1}); + } + + /** + * Resets the transformation to a given description. + * @param transformation The description of the new transformation as array of type double: + * [x, y] or [x, y, a, b, c, d] + * (x, y) is a vector describing the translation. + * | a b | + * | c d | is a linear transformation matrix. + */ + public Texture<T> setAffineTransformation(final double[] transformation) { + if ((transformation.length != TRANSLATION_SIZE) && (transformation.length != TRANSFORMATION_SIZE)) { + throw new IllegalArgumentException("Invalid transformation description."); + } + for (int i = 0; i < Math.min(TRANSFORMATION_SIZE, transformation.length); i++) { + mAffineTransformation[i] = transformation[i]; + } + return this; + } + + /** + * Applies a transformation on top of the current transformation. + * @param transformation The description of the transformation as array of type double: + * [x, y, a, b, c, d] + * (x, y) is a vector describing the translation. + * | a b | + * | c d | is a linear transformation matrix. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public Texture<T> applyAffineTransformation(final double[] transformation) { + if (transformation.length != TRANSFORMATION_SIZE) { + throw new IllegalArgumentException("Invalid transformation description."); + } + double x, y, a, b, c, d; + x = mAffineTransformation[0] + transformation[0]; + y = mAffineTransformation[1] + transformation[1]; + a = mAffineTransformation[2] * transformation[2] + mAffineTransformation[4] * transformation[3]; + b = mAffineTransformation[3] * transformation[2] + mAffineTransformation[5] * transformation[3]; + c = mAffineTransformation[2] * transformation[4] + mAffineTransformation[4] * transformation[5]; + d = mAffineTransformation[3] * transformation[4] + mAffineTransformation[5] * transformation[5]; + setAffineTransformation(new double[]{x, y, a, b, c, d}); + return this; + } + + /** + * Returns a description of the currently applied transformation. + * @return An array of type double in the format [x, y, a, b, c, d] + */ + public double[] getAffineTransformation() { + return mAffineTransformation; + } + + /** + * Returns the width of the texture. + * @return The width of the texture, after which the texture repeats for x-coordinates bigger than this value. + */ + public int getWidth() { + return mWidth; + } + + /** + * Returns the height of the texture. + * @return The height of the texture, after which the texture repeats for y-coordinates bigger than this value. + */ + public int getHeight() { + return mHeight; + } + + /** + * Returns the value of the texture at given coordinates. The coordinates can be bigger than the + * respective texture size, the texture will repeat itself. The given coordinates are treated with + * the current applied affine transformation. + * @param x The x-coordinate of the retrieved values point. + * @param y The y-coordinate of the retrieved values point. + * @return A pixel value of type T. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public T getTextureValueAt(final int x, final int y) { + // affine transformation + double vx = (x + mAffineTransformation[0]); + double vy = (y + mAffineTransformation[1]); + int tx = (int) Math.floor(vx * mAffineTransformation[2] + vy * mAffineTransformation[3]); + int ty = (int) Math.floor(vx * mAffineTransformation[4] + vy * mAffineTransformation[5]); + // read value + T[] row = mTexturePattern[modulo(ty, mHeight)]; + return row[modulo(tx, row.length)]; + } + + private int validatePatternSize(final int size) { + if (size < 1) { + throw new IllegalArgumentException("The given texture pattern is empty"); + } + return size; + } + + private int modulo(final int n, final int mod) { + int r = n % mod; + if (r < 0) { + r += mod; + } + return r; + } + +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TextureRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TextureRasterizer.java new file mode 100644 index 0000000000000000000000000000000000000000..8d843d50bf61de2ef364338152409569ec046179 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TextureRasterizer.java @@ -0,0 +1,36 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +import de.tudresden.inf.mci.brailleplot.layout.InsufficientRenderingAreaException; +import de.tudresden.inf.mci.brailleplot.layout.RasterCanvas; +import de.tudresden.inf.mci.brailleplot.layout.Rectangle; +import de.tudresden.inf.mci.brailleplot.printabledata.MatrixData; + +/** + * A rasterizer that is able to fill an area with a texture specified by a {@link TexturedArea}. + * @author Leonard Kupper + * @version 2019.08.23 + */ +public class TextureRasterizer implements Rasterizer<TexturedArea> { + + /** + * Rasterizes a {@link TexturedArea} instance onto a {@link RasterCanvas}. + * @param data An instance of {@link TexturedArea} representing the area to be filled with a texture. + * @param canvas An instance of {@link RasterCanvas} representing the target for the rasterizer output. + */ + @Override + public void rasterize(final TexturedArea data, final RasterCanvas canvas) throws InsufficientRenderingAreaException { + Rectangle.IntWrapper area = data.getArea().intWrapper(); + MatrixData<Boolean> page = canvas.getCurrentPage(); + + Texture<Boolean> texture = data.getTexture(); + for (int y = 0; y < area.getHeight(); y++) { + for (int x = 0; x < area.getWidth(); x++) { + Boolean value = texture.getTextureValueAt(x, y); + page.setValue(y + area.getY(), x + area.getX(), value); + } + } + + } + + +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TexturedArea.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TexturedArea.java new file mode 100644 index 0000000000000000000000000000000000000000..482a61dca33cac2e7d865f31ab53c493c4b6cf20 --- /dev/null +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/TexturedArea.java @@ -0,0 +1,127 @@ +package de.tudresden.inf.mci.brailleplot.rendering; + +import de.tudresden.inf.mci.brailleplot.layout.Rectangle; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.util.Objects; + +/** + * Representation of an area to be filled with a specific texture. + * Furthermore container for predefined texture patterns and helper methods for texture creation. + * @author Leonard Kupper + * @version 2019.08.23 + */ +public class TexturedArea implements Renderable { + + private Rectangle mArea; + private Texture<Boolean> mTexture; + + // Predefined texture patterns + public static final Boolean[][] UNIFORM_PATTERN = { + {false, false}, + {false, true}, + {false, false}, + {true, true} + }; + public static final Boolean[][] BOTTOM_T_PATTERN = { + {false, false, false}, + {false, true, false}, + {false, true, false}, + {true, true, true} + }; + public static final Boolean[][] DOTTED_PATTERN = { + {false, false}, + {false, true}, + {false, false}, + {true, false} + }; + public static final Boolean[][] LETTER_Y_PATTERN = { + {true, false, true}, + {false, true, false}, + {false, true, false}, + {false, false, false} + }; + public static final Boolean[][] DASHED_PATTERN = { + {false, false, false}, + {false, true, true}, + {false, true, true}, + {false, true, true} + }; + public static final Boolean[][] LINE_PATTERN = { + {false}, + {false}, + {false}, + {true} + }; + public static final Boolean[][] GRID_PATTERN = { + {true, true, true}, + {false, true, false}, + {false, true, false}, + {false, true, false} + }; + + /** + * Texture factory method to create a texture from a bitmap. + * @param imageFile A file object representing the texture bitmap. Each non-white pixel in the bitmap will + * cause a pixel to be set in the resulting monochrome texture. + * @return A {@link Texture}<Boolean> containing the bitmaps texture pattern. + * @throws java.io.IOException On any error while reading the bitmap into a {@link BufferedImage}. + */ + public static Texture<Boolean> textureFromImage(final File imageFile) throws java.io.IOException { + BufferedImage imgBuf = ImageIO.read(Objects.requireNonNull(imageFile)); + Boolean[][] texturePattern = new Boolean[imgBuf.getHeight()][imgBuf.getWidth()]; + final int white = -0x000001; // Java does not know unsigned ints..... + for (int y = 0; y < imgBuf.getHeight(); y++) { + for (int x = 0; x < imgBuf.getWidth(); x++) { + System.out.println(imgBuf.getRGB(x, y)); + texturePattern[y][x] = (imgBuf.getRGB(x, y) != white); + } + } + return new Texture<>(texturePattern); + } + + + /** + * Constructor. Creates a representation of a texture filled area. + * @param texture The areas texture represented by a {@link Texture} instance. + * @param area The desired area for the texture to be rendered on. + */ + public TexturedArea(final Texture<Boolean> texture, final Rectangle area) { + setTexture(texture); + setArea(area); + } + + /** + * Sets a new texture. + * @param texture The areas texture represented by a {@link Texture} instance. + */ + public void setTexture(final Texture<Boolean> texture) { + mTexture = Objects.requireNonNull(texture); + } + + /** + * Gets the currently set texture. + * @return A {@link Texture} instance representing the texture. + */ + public Texture<Boolean> getTexture() { + return mTexture; + } + + /** + * Sets a new area for the texture. + * @param area The new area which the texture fills. + */ + public void setArea(final Rectangle area) { + mArea = Objects.requireNonNull(area); + } + + /** + * Gets the current area of the texture. + * @return The area to be filled with the texture. + */ + public Rectangle getArea() { + return mArea; + } +} diff --git a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/UniformTextureBarChartRasterizer.java b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/UniformTextureBarChartRasterizer.java index 05eaadda4f8f09f7ce93b378b81562e3d27c596b..5bf730ec76ee724048234aa40f2d558f72df8f1f 100644 --- a/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/UniformTextureBarChartRasterizer.java +++ b/src/main/java/de/tudresden/inf/mci/brailleplot/rendering/UniformTextureBarChartRasterizer.java @@ -223,7 +223,7 @@ final class UniformTextureBarChartRasterizer implements Rasterizer<BarChart> { barThickness -= 2; if (barThickness < mBarMinThickness) { throw new InsufficientRenderingAreaException("Not enough space to render given amount of categories in " - + "bar chart. " + mDiagram.getCategoryCount() + " categories given. " + requiredCells + + "bar chart. " + mDiagram.getDataSet().getSize() + " categories given. " + requiredCells + " cells required but only " + availableCells + " available. " + "(Minimum bar thickness is set to " + mBarMinThickness + " dots)"); } @@ -241,7 +241,7 @@ final class UniformTextureBarChartRasterizer implements Rasterizer<BarChart> { int barCells = (int) ceil(barSize / (double) cellHeight); // important cast, else int division happens int cellsInclusive = (int) ceil(sizeInclusive / (double) cellHeight); // --> Linear equation - return barCells + (cellsInclusive - 1) * (mDiagram.getCategoryCount() - 1); + return barCells + (cellsInclusive - 1) * (mDiagram.getDataSet().getSize() - 1); } @@ -269,6 +269,16 @@ final class UniformTextureBarChartRasterizer implements Rasterizer<BarChart> { Rasterizer.rectangle(lowerX, lowerY, upperX, upperY, mData, true); // then the rectangle is filled with the uniform texture + /* + Texture<Boolean> uniformTexture = new Texture<>(TexturedArea.BOTTOM_T_PATTERN); + TextureRasterizer textureRasterizer = new TextureRasterizer(); + TexturedArea barFace = new TexturedArea( + uniformTexture, + new Rectangle(lowerX + 1, upperY + 1, length - 2, thickness - 2) + ); + textureRasterizer.rasterize(barFace, mCanvas); + */ + int textureStep = Integer.signum(upperX - lowerX) * mTextureUnitSize; int i = 0; for (int dotX = lowerX; dotX != upperX; dotX += textureStep) { diff --git a/src/main/resources/examples/csv/0_bar_chart_categorical_vertical.csv b/src/main/resources/examples/csv/0_bar_chart_categorical_vertical.csv index 4af2062281540534bcc3806841c68153c08aad24..8a3987fddecb84c3ab0460517f2e0a11fd0bcca2 100644 --- a/src/main/resources/examples/csv/0_bar_chart_categorical_vertical.csv +++ b/src/main/resources/examples/csv/0_bar_chart_categorical_vertical.csv @@ -1,5 +1,4 @@ Kat.1,Kat.2,Kat.3 Reihe a,3,4,"4,5" Reihe b,"2,5",3,3 -Reihe c,1,2,1 -Reihe d,3,5,"0,2" +Reihe c,1,2,1 \ No newline at end of file