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