source: josm/trunk/test/functional/org/openstreetmap/josm/gui/mappaint/MapCSSRendererTest.java@ 16982

Last change on this file since 16982 was 16982, checked in by simon04, 4 years ago

see #19706, see #19725 - ImageProviderTest: add tests comparing the result with reference images

  • Property svn:eol-style set to native
File size: 17.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.mappaint;
3
4import static org.junit.Assert.assertEquals;
5import static org.junit.Assert.fail;
6
7import java.awt.Color;
8import java.awt.GraphicsEnvironment;
9import java.awt.Point;
10import java.awt.image.BufferedImage;
11import java.io.File;
12import java.io.IOException;
13import java.io.UncheckedIOException;
14import java.nio.file.Files;
15import java.nio.file.Paths;
16import java.text.MessageFormat;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.List;
22import java.util.Locale;
23import java.util.function.Consumer;
24import java.util.stream.Collectors;
25import java.util.stream.Stream;
26
27import javax.imageio.ImageIO;
28
29import org.junit.Assume;
30import org.junit.Before;
31import org.junit.Rule;
32import org.junit.Test;
33import org.junit.runner.RunWith;
34import org.junit.runners.Parameterized;
35import org.junit.runners.Parameterized.Parameters;
36import org.openstreetmap.josm.TestUtils;
37import org.openstreetmap.josm.data.Bounds;
38import org.openstreetmap.josm.data.ProjectionBounds;
39import org.openstreetmap.josm.data.osm.DataSet;
40import org.openstreetmap.josm.data.osm.OsmPrimitive;
41import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer;
42import org.openstreetmap.josm.data.projection.ProjectionRegistry;
43import org.openstreetmap.josm.io.IllegalDataException;
44import org.openstreetmap.josm.io.OsmReader;
45import org.openstreetmap.josm.testutils.JOSMTestRules;
46import org.openstreetmap.josm.tools.ColorHelper;
47
48import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
49
50/**
51 * Test cases for {@link StyledMapRenderer} and the MapCSS classes.
52 * <p>
53 * This test uses the data and reference files stored in the test data directory {@value #TEST_DATA_BASE}
54 * @author Michael Zangl
55 */
56@RunWith(Parameterized.class)
57public class MapCSSRendererTest {
58 private static final String TEST_DATA_BASE = "/renderer/";
59 /**
60 * lat = 0..1, lon = 0..1
61 */
62 private static final Bounds AREA_DEFAULT = new Bounds(0, 0, 1, 1);
63 private static final int IMAGE_SIZE = 256;
64
65 /**
66 * Minimal test rules required
67 */
68 @Rule
69 @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
70 public JOSMTestRules test = new JOSMTestRules().preferences().projection();
71
72 private final TestConfig testConfig;
73
74 // development flag - set to true in order to update all reference images
75 private static final boolean UPDATE_ALL = false;
76
77 /**
78 * The different configurations of this test.
79 *
80 * @return The parameters.
81 */
82 @Parameters(name = "{1}")
83 public static Collection<Object[]> runs() {
84 return Stream.of(
85 /** Tests for StyledMapRenderer#drawNodeSymbol */
86 new TestConfig("node-shapes", AREA_DEFAULT)
87 .setThresholdPixels(100).setThresholdTotalColorDiff(2_110),
88
89 /** Text for nodes */
90 new TestConfig("node-text", AREA_DEFAULT).usesFont("DejaVu Sans")
91 .setThresholdPixels(530).setThresholdTotalColorDiff(23_800),
92
93 /** Tests that StyledMapRenderer#drawWay respects width */
94 new TestConfig("way-width", AREA_DEFAULT)
95 .setThresholdPixels(280).setThresholdTotalColorDiff(22_500),
96
97 /** Tests the way color property, including alpha */
98 new TestConfig("way-color", AREA_DEFAULT)
99 .setThresholdPixels(100).setThresholdTotalColorDiff(3_400),
100
101 /** Tests dashed ways. */
102 new TestConfig("way-dashes", AREA_DEFAULT)
103 .setThresholdPixels(460).setThresholdTotalColorDiff(12_100),
104
105 /** Tests dashed way clamping algorithm */
106 new TestConfig("way-dashes-clamp", AREA_DEFAULT)
107 .setThresholdPixels(200).setThresholdTotalColorDiff(6_800),
108
109 /** Tests fill-color property */
110 new TestConfig("area-fill-color", AREA_DEFAULT),
111
112 /** Tests the fill-image property. */
113 new TestConfig("area-fill-image", AREA_DEFAULT)
114 .setThresholdPixels(420).setThresholdTotalColorDiff(11_200),
115
116 /** Tests area label drawing/placement */
117 new TestConfig("area-text", AREA_DEFAULT)
118 .setThresholdPixels(550).setThresholdTotalColorDiff(17_400),
119
120 /** Tests area icon drawing/placement */
121 new TestConfig("area-icon", AREA_DEFAULT)
122 .setThresholdPixels(680).setThresholdTotalColorDiff(23_000),
123
124 /** Tests if all styles are sorted correctly. Tests {@link StyleRecord#compareTo(StyleRecord)} */
125 new TestConfig("order", AREA_DEFAULT)
126 .setThresholdPixels(2050).setThresholdTotalColorDiff(101_800),
127
128 /** Tests repeat-image feature for ways */
129 new TestConfig("way-repeat-image", AREA_DEFAULT)
130 .setThresholdPixels(2100).setThresholdTotalColorDiff(93_000),
131 /** Tests the clamping for repeat-images and repeat-image-phase */
132 new TestConfig("way-repeat-image-clamp", AREA_DEFAULT)
133 .setThresholdPixels(80).setThresholdTotalColorDiff(2_300),
134
135 /** Tests text along a way */
136 new TestConfig("way-text", AREA_DEFAULT)
137 .setThresholdPixels(3400).setThresholdTotalColorDiff(122_700),
138
139 /** Another test for node shapes */
140 new TestConfig("node-shapes2").setImageWidth(600)
141 .setThresholdPixels(1230).setThresholdTotalColorDiff(43_700),
142 /** Tests default values for node shapes */
143 new TestConfig("node-shapes-default")
144 .setThresholdPixels(10).setThresholdTotalColorDiff(270),
145 /** Tests node shapes with both fill and stroke combined */
146 new TestConfig("node-shapes-combined")
147 .setThresholdPixels(360).setThresholdTotalColorDiff(9_200),
148 /** Another test for dashed ways */
149 new TestConfig("way-dashes2")
150 .setThresholdPixels(340).setThresholdTotalColorDiff(16_100),
151 /** Tests node text placement */
152 new TestConfig("node-text2")
153 .setThresholdPixels(1020).setThresholdTotalColorDiff(345_000),
154 /** Tests relation link selector */
155 new TestConfig("relation-linkselector")
156 .setThresholdPixels(430).setThresholdTotalColorDiff(13_000),
157 /** Tests parent selector on relation */
158 new TestConfig("relation-parentselector")
159 .setThresholdPixels(310).setThresholdTotalColorDiff(8_200),
160
161 /** Tests evaluation of expressions */
162 new TestConfig("eval").setImageWidth(600)
163 .setThresholdPixels(6610).setThresholdTotalColorDiff(3_304_000)
164
165 ).map(e -> new Object[] {e, e.testDirectory})
166 .collect(Collectors.toList());
167 }
168
169 /**
170 * @param testConfig The config to use for this test.
171 * @param ignored The name to print it nicely
172 */
173 public MapCSSRendererTest(TestConfig testConfig, String ignored) {
174 this.testConfig = testConfig;
175 }
176
177 /**
178 * This test only runs on OpenJDK.
179 * It is ignored for other Java versions since they differ slightly in their rendering engine.
180 * @since 11691
181 */
182 @Before
183 public void forOpenJDK() {
184 String javaHome = System.getProperty("java.home");
185 Assume.assumeTrue("Test requires openJDK", javaHome != null && javaHome.toLowerCase(Locale.ENGLISH).contains("openjdk"));
186
187 List<String> fonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
188 for (String font : testConfig.fonts) {
189 Assume.assumeTrue("Test requires font: " + font, fonts.contains(font));
190 }
191 }
192
193 /**
194 * Run the test using {@link #testConfig}
195 * @throws Exception if an error occurs
196 */
197 @Test
198 public void testRender() throws Exception {
199 // Force reset of preferences
200 StyledMapRenderer.PREFERENCE_ANTIALIASING_USE.put(true);
201 StyledMapRenderer.PREFERENCE_TEXT_ANTIALIASING.put("gasp");
202
203 // load the data
204 DataSet dataSet = testConfig.getOsmDataSet();
205 dataSet.allPrimitives().forEach(this::loadPrimitiveStyle);
206 dataSet.setSelected(dataSet.allPrimitives().stream().filter(n -> n.isKeyTrue("selected")).collect(Collectors.toList()));
207
208 ProjectionBounds pb = new ProjectionBounds();
209 pb.extend(ProjectionRegistry.getProjection().latlon2eastNorth(testConfig.getTestArea().getMin()));
210 pb.extend(ProjectionRegistry.getProjection().latlon2eastNorth(testConfig.getTestArea().getMax()));
211 double scale = (pb.maxEast - pb.minEast) / testConfig.imageWidth;
212
213 RenderingHelper.StyleData sd = new RenderingHelper.StyleData();
214 sd.styleUrl = testConfig.getStyleSourceUrl();
215 RenderingHelper rh = new RenderingHelper(dataSet, testConfig.getTestArea(), scale, Collections.singleton(sd));
216 rh.setFillBackground(false);
217 rh.setDebugStream(System.out);
218 System.out.println("Running " + getClass() + "[" + testConfig.testDirectory + "]");
219 BufferedImage image = rh.render();
220
221 assertImageEquals(testConfig.testDirectory,
222 testConfig.getReference(), image,
223 testConfig.thresholdPixels, testConfig.thresholdTotalColorDiff, diffImage -> {
224 try {
225 // You can use this to debug:
226 ImageIO.write(image, "png", new File(testConfig.getTestDirectory() + "/test-output.png"));
227 ImageIO.write(diffImage, "png", new File(testConfig.getTestDirectory() + "/test-differences.png"));
228 } catch (IOException ex) {
229 throw new UncheckedIOException(ex);
230 }
231 });
232 }
233
234 /**
235 * Compares the reference image file with the actual images given as {@link BufferedImage}.
236 * @param testIdentifier a test identifier for error messages
237 * @param referenceImageFile the reference image file to be read using {@link ImageIO#read(File)}
238 * @param image the actual image
239 * @param thresholdPixels maximum number of differing pixels
240 * @param thresholdTotalColorDiff maximum sum of color value differences
241 * @param diffImageConsumer a consumer for a rendered image highlighting the differing pixels, may be null
242 * @throws IOException in case of I/O error
243 */
244 public static void assertImageEquals(
245 String testIdentifier, File referenceImageFile, BufferedImage image,
246 int thresholdPixels, int thresholdTotalColorDiff, Consumer<BufferedImage> diffImageConsumer) throws IOException {
247
248 // TODO move to separate class ImageTestUtils
249 if (UPDATE_ALL) {
250 ImageIO.write(image, "png", referenceImageFile);
251 return;
252 }
253 final BufferedImage reference = ImageIO.read(referenceImageFile);
254 assertEquals(image.getWidth(), reference.getWidth());
255 assertEquals(image.getHeight(), reference.getHeight());
256
257 StringBuilder differences = new StringBuilder();
258 ArrayList<Point> differencePoints = new ArrayList<>();
259 int colorDiffSum = 0;
260
261 for (int y = 0; y < reference.getHeight(); y++) {
262 for (int x = 0; x < reference.getWidth(); x++) {
263 int expected = reference.getRGB(x, y);
264 int result = image.getRGB(x, y);
265 int expectedAlpha = expected >> 24;
266 boolean colorsAreSame = expectedAlpha == 0 ? result >> 24 == 0 : expected == result;
267 if (!colorsAreSame) {
268 Color expectedColor = new Color(expected, true);
269 Color resultColor = new Color(result, true);
270 int colorDiff = Math.abs(expectedColor.getRed() - resultColor.getRed())
271 + Math.abs(expectedColor.getGreen() - resultColor.getGreen())
272 + Math.abs(expectedColor.getBlue() - resultColor.getBlue());
273 int alphaDiff = Math.abs(expectedColor.getAlpha() - resultColor.getAlpha());
274 // Ignore small alpha differences due to Java versions, rendering libraries and so on
275 if (alphaDiff <= 20) {
276 alphaDiff = 0;
277 }
278 // Ignore small color differences for the same reasons, but also completely for almost-transparent pixels
279 if (colorDiff <= 15 || resultColor.getAlpha() <= 20) {
280 colorDiff = 0;
281 }
282 if (colorDiff + alphaDiff > 0) {
283 differencePoints.add(new Point(x, y));
284 if (differences.length() < 2000) {
285 differences.append("\nDifference at ")
286 .append(x)
287 .append(",")
288 .append(y)
289 .append(": Expected ")
290 .append(ColorHelper.color2html(expectedColor))
291 .append(" but got ")
292 .append(ColorHelper.color2html(resultColor))
293 .append(" (color diff is ")
294 .append(colorDiff)
295 .append(", alpha diff is ")
296 .append(alphaDiff)
297 .append(")");
298 }
299 }
300 colorDiffSum += colorDiff + alphaDiff;
301 }
302 }
303 }
304
305 if (differencePoints.size() > thresholdPixels || colorDiffSum > thresholdTotalColorDiff) {
306 // Add a nice image that highlights the differences:
307 BufferedImage diffImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
308 for (Point p : differencePoints) {
309 diffImage.setRGB(p.x, p.y, 0xffff0000);
310 }
311 if (diffImageConsumer != null) {
312 diffImageConsumer.accept(diffImage);
313 }
314
315 if (differencePoints.size() > thresholdPixels) {
316 fail(MessageFormat.format("Images for test {0} differ at {1} points, threshold is {2}: {3}",
317 testIdentifier, differencePoints.size(), thresholdPixels, differences.toString()));
318 } else {
319 fail(MessageFormat.format("Images for test {0} differ too much in color, value is {1}, permitted threshold is {2}: {3}",
320 testIdentifier, colorDiffSum, thresholdTotalColorDiff, differences.toString()));
321 }
322 }
323 }
324
325 private void loadPrimitiveStyle(OsmPrimitive n) {
326 n.setHighlighted(n.isKeyTrue("highlight"));
327 if (n.isKeyTrue("disabled")) {
328 n.setDisabledState(false);
329 }
330 }
331
332 private static class TestConfig {
333 private final String testDirectory;
334 private Bounds testArea;
335 private final ArrayList<String> fonts = new ArrayList<>();
336 private DataSet ds;
337 private int imageWidth = IMAGE_SIZE;
338 private int thresholdPixels;
339 private int thresholdTotalColorDiff;
340
341 TestConfig(String testDirectory, Bounds testArea) {
342 this.testDirectory = testDirectory;
343 this.testArea = testArea;
344 }
345
346 TestConfig(String testDirectory) {
347 this.testDirectory = testDirectory;
348 }
349
350 public TestConfig setImageWidth(int imageWidth) {
351 this.imageWidth = imageWidth;
352 return this;
353 }
354
355 /**
356 * Set the number of pixels that can differ.
357 *
358 * Needed due to somewhat platform dependent font rendering.
359 * @param thresholdPixels the number of pixels that can differ
360 * @return this object, for convenience
361 */
362 public TestConfig setThresholdPixels(int thresholdPixels) {
363 this.thresholdPixels = thresholdPixels;
364 return this;
365 }
366
367 /**
368 * Set the threshold for total color difference.
369 * Every difference in any color component (and alpha) will be added up and must not exceed this threshold.
370 * Needed due to somewhat platform dependent font rendering.
371 * @param thresholdTotalColorDiff he threshold for total color difference
372 * @return this object, for convenience
373 */
374 public TestConfig setThresholdTotalColorDiff(int thresholdTotalColorDiff) {
375 this.thresholdTotalColorDiff = thresholdTotalColorDiff;
376 return this;
377 }
378
379 public TestConfig usesFont(String string) {
380 this.fonts.add(string);
381 return this;
382 }
383
384 public File getReference() {
385 return new File(getTestDirectory() + "/reference.png");
386 }
387
388 private String getTestDirectory() {
389 return TestUtils.getTestDataRoot() + TEST_DATA_BASE + testDirectory;
390 }
391
392 public String getStyleSourceUrl() {
393 return getTestDirectory() + "/style.mapcss";
394 }
395
396 public DataSet getOsmDataSet() throws IllegalDataException, IOException {
397 if (ds == null) {
398 ds = OsmReader.parseDataSet(Files.newInputStream(Paths.get(getTestDirectory(), "data.osm")), null);
399 }
400 return ds;
401 }
402
403 public Bounds getTestArea() throws IllegalDataException, IOException {
404 if (testArea == null) {
405 testArea = getOsmDataSet().getDataSourceBounds().get(0);
406 }
407 return testArea;
408 }
409
410 @Override
411 public String toString() {
412 return "TestConfig [testDirectory=" + testDirectory + ", testArea=" + testArea + ']';
413 }
414 }
415}
Note: See TracBrowser for help on using the repository browser.