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

Last change on this file since 17275 was 17275, checked in by Don-vip, 3 years ago

see #16567 - upgrade almost all tests to JUnit 5, except those depending on WiremockRule

See https://github.com/tomakehurst/wiremock/issues/684

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