|
| 1 | +package de.gsi.chart.renderer.spi; |
| 2 | + |
| 3 | +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
| 4 | +import static org.junit.jupiter.api.Assertions.assertEquals; |
| 5 | +import static org.junit.jupiter.api.Assertions.assertFalse; |
| 6 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 7 | +import static org.junit.jupiter.api.Assertions.assertNull; |
| 8 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
| 9 | + |
| 10 | +import static de.gsi.chart.ui.utils.FuzzyTestImageUtils.compareAndWriteReference; |
| 11 | +import static de.gsi.chart.ui.utils.FuzzyTestImageUtils.writeTestImage; |
| 12 | + |
| 13 | +import java.util.ArrayDeque; |
| 14 | +import java.util.ArrayList; |
| 15 | +import java.util.List; |
| 16 | + |
| 17 | +import javafx.application.Platform; |
| 18 | +import javafx.scene.Scene; |
| 19 | +import javafx.scene.image.Image; |
| 20 | +import javafx.scene.layout.GridPane; |
| 21 | +import javafx.scene.layout.Priority; |
| 22 | +import javafx.stage.Stage; |
| 23 | + |
| 24 | +import org.junit.jupiter.api.extension.ExtendWith; |
| 25 | +import org.slf4j.Logger; |
| 26 | +import org.slf4j.LoggerFactory; |
| 27 | +import org.testfx.framework.junit5.ApplicationExtension; |
| 28 | +import org.testfx.framework.junit5.Start; |
| 29 | + |
| 30 | +import de.gsi.chart.Chart; |
| 31 | +import de.gsi.chart.XYChart; |
| 32 | +import de.gsi.chart.axes.spi.DefaultNumericAxis; |
| 33 | +import de.gsi.chart.renderer.LineStyle; |
| 34 | +import de.gsi.chart.ui.utils.JavaFXInterceptorUtils; |
| 35 | +import de.gsi.chart.ui.utils.TestFx; |
| 36 | +import de.gsi.chart.utils.FXUtils; |
| 37 | +import de.gsi.dataset.DataSet; |
| 38 | +import de.gsi.dataset.spi.AbstractErrorDataSet; |
| 39 | +import de.gsi.dataset.spi.TransposedDataSet; |
| 40 | +import de.gsi.dataset.testdata.spi.GaussFunction; |
| 41 | +import de.gsi.math.DataSetMath; |
| 42 | +import de.gsi.math.MathDataSet; |
| 43 | + |
| 44 | +/** |
| 45 | + * Tests {@link de.gsi.chart.renderer.spi.HistogramRendererTests } |
| 46 | + * |
| 47 | + * @author rstein |
| 48 | + * |
| 49 | + */ |
| 50 | +@ExtendWith(ApplicationExtension.class) |
| 51 | +@ExtendWith(JavaFXInterceptorUtils.SelectiveJavaFxInterceptor.class) |
| 52 | +public class HistogramRendererTests { |
| 53 | + private static final Class<?> clazz = HistogramRendererTests.class; |
| 54 | + private static final Logger LOGGER = LoggerFactory.getLogger(clazz); |
| 55 | + private static final String className = clazz.getSimpleName(); |
| 56 | + private static final String referenceFileName = "Reference_" + className; |
| 57 | + private static final String referenceFileExtension = ".png"; |
| 58 | + private static final int MAX_TIMEOUT_MILLIS = 1000; |
| 59 | + private static final int WAIT_N_FX_PULSES = 3; |
| 60 | + private static final double IMAGE_CMP_THRESHOLD = 0.5; // 1.0 is perfect identity |
| 61 | + private static final int WIDTH = 1000; |
| 62 | + private static final int HEIGHT = 1000; |
| 63 | + private static final int N_SAMPLES = 15; |
| 64 | + private GridPane root; |
| 65 | + private Image testImage; |
| 66 | + |
| 67 | + @Start |
| 68 | + public void start(Stage stage) { |
| 69 | + assertDoesNotThrow(HistogramRenderer::new); |
| 70 | + |
| 71 | + root = new GridPane(); |
| 72 | + // bar plots |
| 73 | + root.addRow(0, getChart(true, false, false, LineStyle.NONE, true), getChart(false, false, false, LineStyle.NONE, true), getChart(false, true, false, LineStyle.NONE, true)); |
| 74 | + root.addRow(1, getChart(true, false, true, LineStyle.NONE, true), getChart(false, false, true, LineStyle.NONE, true), getChart(false, true, true, LineStyle.NONE, true)); |
| 75 | + |
| 76 | + // histogram plots |
| 77 | + root.addRow(2, getChart(true, false, false, LineStyle.BEZIER_CURVE, false), getChart(true, true, false, LineStyle.HISTOGRAM, false), getChart(false, true, false, LineStyle.HISTOGRAM_FILLED, false)); |
| 78 | + root.addRow(3, getChart(true, false, true, LineStyle.BEZIER_CURVE, false), getChart(true, true, true, LineStyle.HISTOGRAM, false), getChart(false, true, true, LineStyle.HISTOGRAM_FILLED, false)); |
| 79 | + |
| 80 | + stage.setScene(new Scene(root, WIDTH, HEIGHT)); |
| 81 | + stage.setTitle(getClass().getSimpleName()); |
| 82 | + stage.setOnCloseRequest(evt -> Platform.exit()); |
| 83 | + stage.show(); |
| 84 | + } |
| 85 | + |
| 86 | + @TestFx |
| 87 | + void basicInterfaceTests() { |
| 88 | + final HistogramRenderer renderer = new HistogramRenderer(); |
| 89 | + |
| 90 | + assertFalse(renderer.isAnimate()); |
| 91 | + assertNotNull(renderer.animateProperty()); |
| 92 | + assertDoesNotThrow(() -> renderer.setAnimate(true)); |
| 93 | + assertTrue(renderer.isAnimate()); |
| 94 | + |
| 95 | + assertTrue(renderer.isAutoSorting()); |
| 96 | + assertNotNull(renderer.autoSortingProperty()); |
| 97 | + assertDoesNotThrow(() -> renderer.setAutoSorting(false)); |
| 98 | + assertFalse(renderer.isAutoSorting()); |
| 99 | + |
| 100 | + final XYChart chart = new XYChart(); |
| 101 | + assertNotNull(renderer.chartProperty()); |
| 102 | + assertNull(renderer.getChart()); |
| 103 | + assertDoesNotThrow(renderer::requestLayout); |
| 104 | + assertDoesNotThrow(() -> renderer.setChartChart(chart)); |
| 105 | + assertEquals(chart, renderer.getChart()); |
| 106 | + assertDoesNotThrow(renderer::requestLayout); |
| 107 | + |
| 108 | + assertTrue(renderer.isRoundedCorner()); |
| 109 | + assertNotNull(renderer.roundedCornerProperty()); |
| 110 | + assertDoesNotThrow(() -> renderer.setRoundedCorner(false)); |
| 111 | + assertFalse(renderer.isRoundedCorner()); |
| 112 | + |
| 113 | + assertEquals(10, renderer.getRoundedCornerRadius()); |
| 114 | + assertNotNull(renderer.roundedCornerRadiusProperty()); |
| 115 | + assertDoesNotThrow(() -> renderer.setRoundedCornerRadius(20)); |
| 116 | + assertEquals(20, renderer.getRoundedCornerRadius()); |
| 117 | + } |
| 118 | + |
| 119 | + public static Chart getChart(final boolean shifted, final boolean stacked, final boolean vertical, final LineStyle lineStyle, final boolean drawBars) { |
| 120 | + final HistogramRenderer renderer = new HistogramRenderer(); |
| 121 | + renderer.setDrawBars(drawBars); |
| 122 | + renderer.setPolyLineStyle(lineStyle); |
| 123 | + if (drawBars) { |
| 124 | + renderer.setPolyLineStyle(LineStyle.NONE); |
| 125 | + } else { |
| 126 | + renderer.setDrawBars(false); |
| 127 | + } |
| 128 | + renderer.setShiftBar(shifted); |
| 129 | + |
| 130 | + renderer.getDatasets().setAll(getTestDataSets(stacked, vertical)); |
| 131 | + if (vertical) { |
| 132 | + renderer.setAutoSorting(false); // N.B. for the time being, auto-sorting needs to be disabled for vertical datasets.... |
| 133 | + } |
| 134 | + |
| 135 | + final DefaultNumericAxis xAxis = new DefaultNumericAxis("abscissa", null); |
| 136 | + final DefaultNumericAxis yAxis = new DefaultNumericAxis("ordinate" + (stacked ? " (stacked)" : ""), null); |
| 137 | + yAxis.setAutoRangeRounding(false); |
| 138 | + yAxis.setAutoRangePadding(0.3); |
| 139 | + |
| 140 | + final XYChart chart; |
| 141 | + chart = new XYChart(vertical ? yAxis : xAxis, vertical ? xAxis : yAxis); |
| 142 | + chart.getRenderers().set(0, renderer); |
| 143 | + chart.legendVisibleProperty().set(true); |
| 144 | + chart.setLegendVisible(false); |
| 145 | + GridPane.setHgrow(chart, Priority.ALWAYS); |
| 146 | + GridPane.setVgrow(chart, Priority.ALWAYS); |
| 147 | + |
| 148 | + return chart; |
| 149 | + } |
| 150 | + |
| 151 | + private String getReferenceImageFileName() { |
| 152 | + final String options = "_default"; |
| 153 | + return referenceFileName + options + referenceFileExtension; |
| 154 | + } |
| 155 | + |
| 156 | + @TestFx |
| 157 | + void testRenderer() throws Exception { |
| 158 | + final String referenceImage = getReferenceImageFileName(); |
| 159 | + FXUtils.runAndWait(() -> testImage = root.snapshot(null, null)); |
| 160 | + final double tresholdIdentity = compareAndWriteReference(clazz, referenceImage, testImage); |
| 161 | + if (IMAGE_CMP_THRESHOLD < tresholdIdentity) { |
| 162 | + if (LOGGER.isTraceEnabled()) { |
| 163 | + LOGGER.atInfo().addArgument(tresholdIdentity).log("image identity - threshold = {}"); |
| 164 | + } |
| 165 | + } else { |
| 166 | + // write image to report repository |
| 167 | + writeTestImage(clazz, "Test_" + clazz.getSimpleName() + "_identity.png", testImage); |
| 168 | + if (LOGGER.isTraceEnabled()) { |
| 169 | + LOGGER.atWarn().addArgument(tresholdIdentity).log("image identity - threshold exceeded = {}"); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + FXUtils.runAndWait(() -> root.requestLayout()); |
| 174 | + assertTrue(FXUtils.waitForFxTicks(root.getScene(), WAIT_N_FX_PULSES, MAX_TIMEOUT_MILLIS)); |
| 175 | + |
| 176 | + FXUtils.runAndWait(() -> testImage = root.snapshot(null, null)); |
| 177 | + final double tresholdNonIdentity = compareAndWriteReference(clazz, referenceImage, testImage); |
| 178 | + if (IMAGE_CMP_THRESHOLD > tresholdNonIdentity) { |
| 179 | + if (LOGGER.isTraceEnabled()) { |
| 180 | + LOGGER.atInfo().addArgument(tresholdNonIdentity).log("image non-identity - threshold = {}"); |
| 181 | + } |
| 182 | + } else { |
| 183 | + // write image to report repository |
| 184 | + writeTestImage(clazz, "Test_" + clazz.getSimpleName() + "_nonidentity.png", testImage); |
| 185 | + if (LOGGER.isTraceEnabled()) { |
| 186 | + LOGGER.atWarn().addArgument(tresholdNonIdentity).log("image non-identity - threshold exceeded = {}"); |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + private static List<DataSet> getTestDataSets(final boolean stacked, final boolean transposed) { |
| 192 | + final List<DataSet> dataSets = new ArrayList<>(); |
| 193 | + for (int centre : new int[] { 2 * N_SAMPLES / 5, N_SAMPLES / 3, 2 * N_SAMPLES / 3 }) { |
| 194 | + final AbstractErrorDataSet<?> gauss = new GaussFunction("h" + centre, N_SAMPLES, centre, 0.1 * N_SAMPLES); |
| 195 | + gauss.addDataLabel(centre, "special point for " + gauss.getName()); |
| 196 | + gauss.addDataStyle(centre, "strokeColor=cyan; fillColor=cyan; markerColor=cyan;"); |
| 197 | + dataSets.add(gauss); |
| 198 | + } |
| 199 | + if (stacked) { |
| 200 | + final SummingDataSet sum123 = new SummingDataSet("Sum", new SummingDataSet("Sum", dataSets.toArray(new DataSet[0]))); |
| 201 | + final SummingDataSet sum12 = new SummingDataSet("Sum", new SummingDataSet("Sum", dataSets.subList(0, 1).toArray(new DataSet[0]))); |
| 202 | + dataSets.set(0, sum123); |
| 203 | + dataSets.set(1, sum12); |
| 204 | + } |
| 205 | + |
| 206 | + if (transposed) { |
| 207 | + dataSets.set(0, TransposedDataSet.transpose(dataSets.get(0))); |
| 208 | + dataSets.set(1, TransposedDataSet.transpose(dataSets.get(1))); |
| 209 | + dataSets.set(2, TransposedDataSet.transpose(dataSets.get(2))); |
| 210 | + } |
| 211 | + |
| 212 | + return dataSets; |
| 213 | + } |
| 214 | + |
| 215 | + public static class SummingDataSet extends MathDataSet { // NOSONAR NOPMD -- too many parents is out of our control (Java intrinsic) |
| 216 | + public SummingDataSet(final String name, final DataSet... functions) { |
| 217 | + super(name, (dataSets, returnFunction) -> { |
| 218 | + if (dataSets.isEmpty()) { |
| 219 | + return; |
| 220 | + } |
| 221 | + final ArrayDeque<DataSet> lockQueue = new ArrayDeque<>(dataSets.size()); |
| 222 | + try { |
| 223 | + dataSets.forEach(ds -> { |
| 224 | + lockQueue.push(ds); |
| 225 | + ds.lock().readLock(); |
| 226 | + }); |
| 227 | + returnFunction.clearData(); |
| 228 | + final DataSet firstDataSet = dataSets.get(0); |
| 229 | + returnFunction.add(firstDataSet.get(DIM_X, 0), 0); |
| 230 | + returnFunction.add(firstDataSet.get(DIM_X, firstDataSet.getDataCount() - 1), 0); |
| 231 | + dataSets.forEach(ds -> returnFunction.set(DataSetMath.addFunction(returnFunction, ds), false)); |
| 232 | + } finally { |
| 233 | + // unlock in reverse order |
| 234 | + while (!lockQueue.isEmpty()) { |
| 235 | + lockQueue.pop().lock().readUnLock(); |
| 236 | + } |
| 237 | + } |
| 238 | + }, functions); |
| 239 | + } |
| 240 | + } |
| 241 | +} |
0 commit comments