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

Last change on this file since 19055 was 19055, checked in by taylor.smock, 4 weeks ago

Fix tests in Java 21

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