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

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

Fix last failing test in Java 21

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