[11433] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
| 2 | package org.openstreetmap.josm.gui.mappaint;
|
---|
| 3 |
|
---|
| 4 | import static org.junit.Assert.assertEquals;
|
---|
| 5 | import static org.junit.Assert.fail;
|
---|
| 6 |
|
---|
[11691] | 7 | import java.awt.Graphics2D;
|
---|
[11433] | 8 | import java.awt.Point;
|
---|
[11691] | 9 | import java.awt.RenderingHints;
|
---|
[11433] | 10 | import java.awt.image.BufferedImage;
|
---|
| 11 | import java.io.File;
|
---|
| 12 | import java.io.FileInputStream;
|
---|
| 13 | import java.io.FileNotFoundException;
|
---|
| 14 | import java.io.IOException;
|
---|
[11691] | 15 | import java.text.MessageFormat;
|
---|
| 16 | import java.util.ArrayList;
|
---|
[11433] | 17 | import java.util.Arrays;
|
---|
| 18 | import java.util.Collection;
|
---|
| 19 | import java.util.stream.Collectors;
|
---|
| 20 | import java.util.stream.Stream;
|
---|
| 21 |
|
---|
| 22 | import javax.imageio.ImageIO;
|
---|
| 23 |
|
---|
[11691] | 24 | import org.junit.Assume;
|
---|
| 25 | import org.junit.Before;
|
---|
[11433] | 26 | import org.junit.Rule;
|
---|
| 27 | import org.junit.Test;
|
---|
| 28 | import org.junit.runner.RunWith;
|
---|
| 29 | import org.junit.runners.Parameterized;
|
---|
| 30 | import org.junit.runners.Parameterized.Parameters;
|
---|
| 31 | import org.openstreetmap.josm.TestUtils;
|
---|
| 32 | import org.openstreetmap.josm.data.Bounds;
|
---|
| 33 | import org.openstreetmap.josm.data.osm.DataSet;
|
---|
| 34 | import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
|
---|
| 35 | import org.openstreetmap.josm.gui.NavigatableComponent;
|
---|
| 36 | import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
|
---|
| 37 | import org.openstreetmap.josm.gui.preferences.SourceEntry;
|
---|
| 38 | import org.openstreetmap.josm.io.IllegalDataException;
|
---|
| 39 | import org.openstreetmap.josm.io.OsmReader;
|
---|
| 40 | import org.openstreetmap.josm.testutils.JOSMTestRules;
|
---|
| 41 |
|
---|
| 42 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
---|
| 43 |
|
---|
| 44 | /**
|
---|
| 45 | * Test cases for {@link StyledMapRenderer} and the MapCSS classes.
|
---|
| 46 | * <p>
|
---|
| 47 | * This test uses the data and reference files stored in the test data directory {@value #TEST_DATA_BASE}
|
---|
| 48 | * @author Michael Zangl
|
---|
| 49 | */
|
---|
| 50 | @RunWith(Parameterized.class)
|
---|
| 51 | public class MapCSSRendererTest {
|
---|
| 52 | private static final String TEST_DATA_BASE = "/renderer/";
|
---|
| 53 | /**
|
---|
| 54 | * lat = 0..1, lon = 0..1
|
---|
| 55 | */
|
---|
| 56 | private static final Bounds AREA_DEFAULT = new Bounds(0, 0, 1, 1);
|
---|
| 57 | private static final int IMAGE_SIZE = 256;
|
---|
| 58 |
|
---|
| 59 | /**
|
---|
| 60 | * Minimal test rules required
|
---|
| 61 | */
|
---|
| 62 | @Rule
|
---|
| 63 | @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
|
---|
| 64 | public JOSMTestRules test = new JOSMTestRules().preferences().projection();
|
---|
| 65 |
|
---|
| 66 | private TestConfig testConfig;
|
---|
| 67 |
|
---|
| 68 | /**
|
---|
| 69 | * The different configurations of this test.
|
---|
| 70 | * @return The parameters.
|
---|
| 71 | */
|
---|
[11691] | 72 | @Parameters(name="{1}")
|
---|
[11436] | 73 | public static Collection<Object[]> runs() {
|
---|
[11433] | 74 | return Stream.of(
|
---|
| 75 | /** Tests for StyledMapRenderer#drawNodeSymbol */
|
---|
| 76 | new TestConfig("node-shapes", AREA_DEFAULT),
|
---|
| 77 |
|
---|
| 78 | /** Tests that StyledMapRenderer#drawWay respects width */
|
---|
| 79 | new TestConfig("way-width", AREA_DEFAULT)
|
---|
| 80 |
|
---|
[11691] | 81 | ).map(e -> new Object[] {e, e.testDirectory})
|
---|
[11433] | 82 | .collect(Collectors.toList());
|
---|
| 83 | }
|
---|
| 84 |
|
---|
| 85 | /**
|
---|
| 86 | * @param testConfig The config to use for this test.
|
---|
[11691] | 87 | * @param ignored The name to print it nicely
|
---|
[11433] | 88 | */
|
---|
[11691] | 89 | public MapCSSRendererTest(TestConfig testConfig, String ignored) {
|
---|
[11433] | 90 | this.testConfig = testConfig;
|
---|
| 91 | }
|
---|
| 92 |
|
---|
| 93 | /**
|
---|
[11691] | 94 | * This test only runs on OpenJDK.
|
---|
| 95 | * It is ignored for other Java versions since they differ slightly in their rendering engine.
|
---|
| 96 | * @since 11691
|
---|
| 97 | */
|
---|
| 98 | @Before
|
---|
| 99 | public void testForOpenJDK() {
|
---|
| 100 | String javaHome = System.getProperty("java.home");
|
---|
| 101 | Assume.assumeTrue(javaHome != null && javaHome.contains("openjdk"));
|
---|
| 102 | }
|
---|
| 103 |
|
---|
| 104 | /**
|
---|
[11433] | 105 | * Run the test using {@link #testConfig}
|
---|
| 106 | * @throws Exception if an error occurs
|
---|
| 107 | */
|
---|
| 108 | @Test
|
---|
[11436] | 109 | public void testRender() throws Exception {
|
---|
[11433] | 110 | // load the data
|
---|
| 111 | DataSet dataSet = testConfig.getOsmDataSet();
|
---|
| 112 |
|
---|
| 113 | // load the style
|
---|
| 114 | MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().lock();
|
---|
| 115 | try {
|
---|
| 116 | MapPaintStyles.getStyles().clear();
|
---|
| 117 |
|
---|
| 118 | MapCSSStyleSource source = new MapCSSStyleSource(testConfig.getStyleSourceEntry());
|
---|
| 119 | source.loadStyleSource();
|
---|
| 120 | if (!source.getErrors().isEmpty()) {
|
---|
| 121 | fail("Failed to load style file. Errors: " + source.getErrors());
|
---|
| 122 | }
|
---|
| 123 | MapPaintStyles.getStyles().setStyleSources(Arrays.asList(source));
|
---|
| 124 |
|
---|
| 125 | } finally {
|
---|
| 126 | MapCSSStyleSource.STYLE_SOURCE_LOCK.writeLock().unlock();
|
---|
| 127 | }
|
---|
| 128 |
|
---|
| 129 | // create the renderer
|
---|
| 130 | BufferedImage image = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB);
|
---|
| 131 | NavigatableComponent nc = new NavigatableComponent() {
|
---|
| 132 | {
|
---|
| 133 | setBounds(0, 0, IMAGE_SIZE, IMAGE_SIZE);
|
---|
| 134 | updateLocationState();
|
---|
| 135 | }
|
---|
| 136 |
|
---|
| 137 | @Override
|
---|
| 138 | protected boolean isVisibleOnScreen() {
|
---|
| 139 | return true;
|
---|
| 140 | }
|
---|
| 141 |
|
---|
| 142 | @Override
|
---|
| 143 | public Point getLocationOnScreen() {
|
---|
| 144 | return new Point(0, 0);
|
---|
| 145 | }
|
---|
| 146 | };
|
---|
| 147 | nc.zoomTo(testConfig.testArea);
|
---|
| 148 | dataSet.allPrimitives().stream().forEach(n -> n.setHighlighted(n.isKeyTrue("highlight")));
|
---|
[11691] | 149 | Graphics2D g = image.createGraphics();
|
---|
| 150 | // Force all render hints to be defaults - do not use platform values
|
---|
| 151 | g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
---|
| 152 | g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
---|
| 153 | g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
---|
| 154 | g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
|
---|
| 155 | g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
---|
| 156 | g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
---|
| 157 | g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
---|
| 158 | g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
|
---|
| 159 | g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
---|
| 160 | new StyledMapRenderer(g, nc, false).render(dataSet, false, testConfig.testArea);
|
---|
[11433] | 161 |
|
---|
| 162 | BufferedImage reference = testConfig.getReference();
|
---|
| 163 |
|
---|
| 164 | // now compute differences:
|
---|
| 165 | assertEquals(IMAGE_SIZE, reference.getWidth());
|
---|
| 166 | assertEquals(IMAGE_SIZE, reference.getHeight());
|
---|
| 167 |
|
---|
| 168 | StringBuilder differences = new StringBuilder();
|
---|
[11691] | 169 | ArrayList<Point> differencePoints = new ArrayList<>();
|
---|
[11433] | 170 |
|
---|
| 171 | for (int y = 0; y < reference.getHeight(); y++) {
|
---|
| 172 | for (int x = 0; x < reference.getWidth(); x++) {
|
---|
| 173 | int expected = reference.getRGB(x, y);
|
---|
| 174 | int result = image.getRGB(x, y);
|
---|
[11691] | 175 | if (!colorsAreSame(expected, result)) {
|
---|
| 176 | differencePoints.add(new Point(x, y));
|
---|
| 177 | if (differences.length() < 500) {
|
---|
| 178 | differences.append("\nDifference at ")
|
---|
| 179 | .append(x)
|
---|
| 180 | .append(",")
|
---|
| 181 | .append(y)
|
---|
| 182 | .append(": Expected ")
|
---|
| 183 | .append(Integer.toHexString(expected))
|
---|
| 184 | .append(" but got ")
|
---|
| 185 | .append(Integer.toHexString(result));
|
---|
| 186 | }
|
---|
[11433] | 187 | }
|
---|
| 188 | }
|
---|
| 189 | }
|
---|
| 190 |
|
---|
[11691] | 191 | if (differencePoints.size() > 0) {
|
---|
[11433] | 192 | // You can use this to debug:
|
---|
| 193 | ImageIO.write(image, "png", new File(testConfig.getTestDirectory() + "/test-output.png"));
|
---|
[11691] | 194 |
|
---|
| 195 | // Add a nice image that highlights the differences:
|
---|
| 196 | BufferedImage diffImage = new BufferedImage(IMAGE_SIZE, IMAGE_SIZE, BufferedImage.TYPE_INT_ARGB);
|
---|
| 197 | for (Point p : differencePoints) {
|
---|
| 198 | diffImage.setRGB(p.x, p.y, 0xffff0000);
|
---|
| 199 | }
|
---|
| 200 | ImageIO.write(diffImage, "png", new File(testConfig.getTestDirectory() + "/test-differences.png"));
|
---|
| 201 |
|
---|
| 202 | fail(MessageFormat.format("Images for test {1} differ at {2} points: {3}",
|
---|
| 203 | testConfig.testDirectory, differencePoints.size(), differences.toString()));
|
---|
[11433] | 204 | }
|
---|
| 205 | }
|
---|
| 206 |
|
---|
[11691] | 207 | /**
|
---|
| 208 | * Check if two colors differ
|
---|
| 209 | * @param expected
|
---|
| 210 | * @param result
|
---|
| 211 | * @return <code>true</code> if they differ.
|
---|
| 212 | */
|
---|
| 213 | private boolean colorsAreSame(int expected, int result) {
|
---|
| 214 | int expectedAlpha = expected >> 24;
|
---|
| 215 | if (expectedAlpha == 0) {
|
---|
| 216 | return (result & 0xff000000) == 0;
|
---|
| 217 | } else {
|
---|
| 218 | return expected == result;
|
---|
| 219 | }
|
---|
| 220 | }
|
---|
| 221 |
|
---|
[11433] | 222 | private static class TestConfig {
|
---|
| 223 | private final String testDirectory;
|
---|
| 224 | private final Bounds testArea;
|
---|
| 225 |
|
---|
| 226 | TestConfig(String testDirectory, Bounds testArea) {
|
---|
| 227 | this.testDirectory = testDirectory;
|
---|
| 228 | this.testArea = testArea;
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | public BufferedImage getReference() throws IOException {
|
---|
| 232 | return ImageIO.read(new File(getTestDirectory() + "/reference.png"));
|
---|
| 233 | }
|
---|
| 234 |
|
---|
| 235 | private String getTestDirectory() {
|
---|
| 236 | return TestUtils.getTestDataRoot() + TEST_DATA_BASE + testDirectory;
|
---|
| 237 | }
|
---|
| 238 |
|
---|
| 239 | public SourceEntry getStyleSourceEntry() {
|
---|
| 240 | return new SourceEntry(getTestDirectory() + "/style.mapcss",
|
---|
| 241 | "test style", "a test style", true // active
|
---|
| 242 | );
|
---|
| 243 | }
|
---|
| 244 |
|
---|
| 245 | public DataSet getOsmDataSet() throws FileNotFoundException, IllegalDataException {
|
---|
| 246 | return OsmReader.parseDataSet(new FileInputStream(getTestDirectory() + "/data.osm"), null);
|
---|
| 247 | }
|
---|
| 248 |
|
---|
| 249 | @Override
|
---|
| 250 | public String toString() {
|
---|
| 251 | return "TestConfig [testDirectory=" + testDirectory + ", testArea=" + testArea + ']';
|
---|
| 252 | }
|
---|
| 253 | }
|
---|
| 254 | }
|
---|