// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.mappaint; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Point; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.imageio.ImageIO; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.openstreetmap.josm.TestUtils; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer; import org.openstreetmap.josm.gui.NavigatableComponent; import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource; import org.openstreetmap.josm.gui.preferences.SourceEntry; import org.openstreetmap.josm.io.IllegalDataException; import org.openstreetmap.josm.io.OsmReader; import org.openstreetmap.josm.testutils.JOSMTestRules; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; /** * Test cases for {@link StyledMapRenderer} and the MapCSS classes. * <p> * This test uses the data and reference files stored in the test data directory {@value #TEST_DATA_BASE} * @author Michael Zangl */ @RunWith(Parameterized.class) public class MapCSSRendererTest { private static final String TEST_DATA_BASE = "/renderer/"; /** * lat = 0..1, lon = 0..1 */ private static final Bounds AREA_DEFAULT = new Bounds(0, 0, 1, 1); private static final int IMAGE_SIZE = 256; /** * Minimal test rules required */ @Rule @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD") public JOSMTestRules test = new JOSMTestRules().preferences().projection(); private TestConfig testConfig; /** * The different configurations of this test. * * @return The parameters. */ @Parameters(name = "{1}") public static Collection<Object[]> runs() { return Stream.of( /** Tests for StyledMapRenderer#drawNodeSymbol */ new TestConfig("node-shapes", AREA_DEFAULT), /** Text for nodes */ new TestConfig("node-text", AREA_DEFAULT).usesFont("DejaVu Sans"), /** Tests that StyledMapRenderer#drawWay respects width */ new TestConfig("way-width", AREA_DEFAULT), /** Tests the way color property, including alpha */ new TestConfig("way-color", AREA_DEFAULT), /** Tests dashed ways. */ new TestConfig("way-dashes", AREA_DEFAULT), /** Tests fill-color property */ new TestConfig("area-fill-color", AREA_DEFAULT), /** Tests the fill-image property. */ new TestConfig("area-fill-image", AREA_DEFAULT), /** Tests area label drawing/placement */ new TestConfig("area-text", AREA_DEFAULT), /** Tests area icon drawing/placement */ new TestConfig("area-icon", AREA_DEFAULT), /** Tests if all styles are sorted correctly. Tests {@link StyleRecord#compareTo(StyleRecord)} */ new TestConfig("order", AREA_DEFAULT) ).map(e -> new Object[] {e, e.testDirectory}) .collect(Collectors.toList()); } /** * @param testConfig The config to use for this test. * @param ignored The name to print it nicely */ public MapCSSRendererTest(TestConfig testConfig, String ignored) { this.testConfig = testConfig; } /** * This test only runs on OpenJDK. * It is ignored for other Java versions since they differ slightly in their rendering engine. * @since 11691 */ @Before public void forOpenJDK() { String javaHome = System.getProperty("java.home"); Assume.assumeTrue("Test requires openJDK", javaHome != null && javaHome.contains("openjdk")); List<String> fonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); for (String font : testConfig.fonts) { Assume.assumeTrue("Test requires font: " + font, fonts.contains(font)); } } /** * Run the test using {@link #testConfig} * @throws Exception if an error occurs */ @Test public void testRender() throws Exception { // Force reset of preferences StyledMapRenderer.PREFERENCE_ANTIALIASING_USE.put(true); StyledMapRenderer.PREFERENCE_TEXT_ANTIALIASING.put("gasp"); // load the data DataSet dataSet = testConfig.getOsmDataSet(); // load the style MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().lock(); try { MapPaintStyles.getStyles().clear(); MapCSSStyleSource source = new MapCSSStyleSource(testConfig.getStyleSourceEntry()); source.loadStyleSource(); if (!source.getErrors().isEmpty()) { fail("Failed to load style file. Errors: " + source.getErrors()); } MapPaintStyles.getStyles().setStyleSources(Arrays.asList(source)); MapPaintStyles.fireMapPaintSylesUpdated(); MapPaintStyles.getStyles().clearCached(); } finally { MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().unlock(); } // create the renderer BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB); NavigatableComponent nc = new NavigatableComponent() { { setBounds(0, 0, IMAGE_SIZE, IMAGE_SIZE); updateLocationState(); } @Override protected boolean isVisibleOnScreen() { return true; } @Override public Point getLocationOnScreen() { return new Point(0, 0); } }; nc.zoomTo(testConfig.testArea); dataSet.allPrimitives().stream().forEach(this::loadPrimitiveStyle); dataSet.setSelected(dataSet.allPrimitives().stream().filter(n -> n.isKeyTrue("selected")).collect(Collectors.toList())); Graphics2D g = image.createGraphics(); // Force all render hints to be defaults - do not use platform values g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); new StyledMapRenderer(g, nc, false).render(dataSet, false, testConfig.testArea); BufferedImage reference = testConfig.getReference(); // now compute differences: assertEquals(IMAGE_SIZE, reference.getWidth()); assertEquals(IMAGE_SIZE, reference.getHeight()); StringBuilder differences = new StringBuilder(); ArrayList<Point> differencePoints = new ArrayList<>(); for (int y = 0; y < reference.getHeight(); y++) { for (int x = 0; x < reference.getWidth(); x++) { int expected = reference.getRGB(x, y); int result = image.getRGB(x, y); if (!colorsAreSame(expected, result)) { differencePoints.add(new Point(x, y)); if (differences.length() < 500) { differences.append("\nDifference at ") .append(x) .append(",") .append(y) .append(": Expected ") .append(Integer.toHexString(expected)) .append(" but got ") .append(Integer.toHexString(result)); } } } } if (differencePoints.size() > 0) { // You can use this to debug: ImageIO.write(image, "png", new File(testConfig.getTestDirectory() + "/test-output.png")); // Add a nice image that highlights the differences: BufferedImage diffImage = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB); for (Point p : differencePoints) { diffImage.setRGB(p.x, p.y, 0xffff0000); } ImageIO.write(diffImage, "png", new File(testConfig.getTestDirectory() + "/test-differences.png")); fail(MessageFormat.format("Images for test {0} differ at {1} points: {2}", testConfig.testDirectory, differencePoints.size(), differences.toString())); } } private void loadPrimitiveStyle(OsmPrimitive n) { n.setHighlighted(n.isKeyTrue("highlight")); if (n.isKeyTrue("disabled")) { n.setDisabledState(false); } } /** * Check if two colors differ * @param expected The expected color * @param actual The actual color * @return <code>true</code> if they differ. */ private boolean colorsAreSame(int expected, int actual) { int expectedAlpha = expected >> 24; if (expectedAlpha == 0) { return actual >> 24 == 0; } else { return expected == actual; } } private static class TestConfig { private final String testDirectory; private final Bounds testArea; private final ArrayList<String> fonts = new ArrayList<>(); TestConfig(String testDirectory, Bounds testArea) { this.testDirectory = testDirectory; this.testArea = testArea; } public TestConfig usesFont(String string) { this.fonts.add(string); return this; } public BufferedImage getReference() throws IOException { return ImageIO.read(new File(getTestDirectory() + "/reference.png")); } private String getTestDirectory() { return TestUtils.getTestDataRoot() + TEST_DATA_BASE + testDirectory; } public SourceEntry getStyleSourceEntry() { return new SourceEntry(getTestDirectory() + "/style.mapcss", "test style", "a test style", true // active ); } public DataSet getOsmDataSet() throws FileNotFoundException, IllegalDataException { return OsmReader.parseDataSet(new FileInputStream(getTestDirectory() + "/data.osm"), null); } @Override public String toString() { return "TestConfig [testDirectory=" + testDirectory + ", testArea=" + testArea + ']'; } } }