Ticket #11153: 11153.4.patch

File 11153.4.patch, 36.4 KB (added by taylor.smock, 3 years ago)

Fix broken tests (add <a href=...) and correctly obtain the ignore group

  • src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java

    Subject: [PATCH] #11153: improve readability of validator warnings
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerFixCommand.java
    a b  
    5454        } else {
    5555            return null;
    5656        }
    57         return MapCSSTagCheckerRule.insertArguments(matchingSelector, s, p);
     57        return MapCSSTagCheckerRule.insertArguments(matchingSelector, s, p, false);
    5858    }
    5959
    6060    /**
     
    113113            @Override
    114114            public Command createCommand(OsmPrimitive p, Selector matchingSelector) {
    115115                return new ChangePropertyKeyCommand(p,
    116                         MapCSSTagCheckerRule.insertArguments(matchingSelector, oldKey, p),
    117                         MapCSSTagCheckerRule.insertArguments(matchingSelector, newKey, p));
     116                        MapCSSTagCheckerRule.insertArguments(matchingSelector, oldKey, p, false),
     117                        MapCSSTagCheckerRule.insertArguments(matchingSelector, newKey, p, false));
    118118            }
    119119
    120120            @Override
  • src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java b/src/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerRule.java
    a b  
    262262     * @param matchingSelector matching selector
    263263     * @param s                any string
    264264     * @param p                OSM primitive
     265     * @param linkify          {@code true} to insert links to wiki pages
    265266     * @return string with arguments inserted
    266267     */
    267     static String insertArguments(Selector matchingSelector, String s, OsmPrimitive p) {
     268    static String insertArguments(Selector matchingSelector, String s, OsmPrimitive p, boolean linkify) {
    268269        if (s != null && matchingSelector instanceof Selector.ChildOrParentSelector) {
    269             return insertArguments(((Selector.ChildOrParentSelector) matchingSelector).right, s, p);
     270            return insertArguments(((Selector.ChildOrParentSelector) matchingSelector).right, s, p, linkify);
    270271        } else if (s == null || !(matchingSelector instanceof Selector.GeneralSelector)) {
    271272            return s;
    272273        }
    273         final Matcher m = Pattern.compile("\\{(\\d+)\\.(key|value|tag)\\}").matcher(s);
     274        final Matcher m = Pattern.compile("\\{(\\S+?)\\.(key|value|tag)}", Pattern.UNICODE_CHARACTER_CLASS).matcher(s);
     275        final Matcher number = Pattern.compile("^\\d+$").matcher(s);
    274276        final StringBuffer sb = new StringBuffer();
    275277        while (m.find()) {
    276             final String argument = determineArgument((Selector.GeneralSelector) matchingSelector,
    277                     Integer.parseInt(m.group(1)), m.group(2), p);
     278            final String argument;
     279            final String tag;
     280            if (number.reset(m.group(1)).matches()) {
     281                argument = determineArgument((Selector.GeneralSelector) matchingSelector,
     282                        Integer.parseInt(m.group(1)), m.group(2), p);
     283                tag = determineArgument((Selector.GeneralSelector) matchingSelector,
     284                        Integer.parseInt(m.group(1)), "tag", p);
     285            } else {
     286                argument = m.group(1);
     287                tag = argument;
     288            }
    278289            try {
    279290                // Perform replacement with null-safe + regex-safe handling
    280                 m.appendReplacement(sb, String.valueOf(argument).replace("^(", "").replace(")$", ""));
     291                final String replacement = String.valueOf(argument).replace("^(", "").replace(")$", "");
     292                final String replacementTag = String.valueOf(tag).replace("^(", "").replace(")$", "");
     293                final String[] splitTag = replacementTag.split("=", 2);
     294                if (linkify) {
     295                    final String url = Test.getWikiLink(splitTag[0], !"key".equals(m.group(2)) && splitTag.length == 2 ? splitTag[1] : null);
     296                    m.appendReplacement(sb, "<a href=\"" + url + "\">"
     297                            + replacement
     298                            + "</a>");
     299                } else {
     300                    m.appendReplacement(sb, replacement);
     301                }
    281302            } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
    282303                Logging.log(Logging.LEVEL_ERROR, tr("Unable to replace argument {0} in {1}: {2}", argument, sb, e.getMessage()), e);
    283304            }
     
    307328                cmds.add(new DeleteCommand(p));
    308329            }
    309330            return cmds.isEmpty() ? null
    310                     : new SequenceCommand(tr("Fix of {0}", getDescriptionForMatchingSelector(p, matchingSelector)), cmds);
     331                    : new SequenceCommand(tr("Fix of {0}", getDescriptionForMatchingSelector(p, matchingSelector, false)), cmds);
    311332        } catch (IllegalArgumentException e) {
    312333            Logging.error(e);
    313334            return null;
     
    356377     *
    357378     * @param matchingSelector matching selector
    358379     * @param p                OSM primitive
     380     * @param linkify          {@code true} to get a description with links to tag/key wiki pages
    359381     * @return a description (possibly with alternative suggestions)
    360382     */
    361     String getDescriptionForMatchingSelector(OsmPrimitive p, Selector matchingSelector) {
    362         return insertArguments(matchingSelector, getDescription(p), p);
     383    String getDescriptionForMatchingSelector(OsmPrimitive p, Selector matchingSelector, boolean linkify) {
     384        return insertArguments(matchingSelector, getDescription(p), p, linkify);
    363385    }
    364386
    365387    Severity getSeverity() {
     
    384406        List<TestError> res = new ArrayList<>();
    385407        if (matchingSelector != null && !errors.isEmpty()) {
    386408            final Command fix = fixPrimitive(p);
    387             final String description = getDescriptionForMatchingSelector(p, matchingSelector);
     409            final String description = getDescriptionForMatchingSelector(p, matchingSelector, true);
    388410            final String description1 = group == null ? description : group;
    389411            final String description2 = group == null ? null : description;
    390412            final String selector = matchingSelector.toString();
  • src/org/openstreetmap/josm/data/validation/Test.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/Test.java b/src/org/openstreetmap/josm/data/validation/Test.java
    a b  
    1111import java.util.function.Predicate;
    1212import java.util.stream.Collectors;
    1313
     14import javax.annotation.Nonnull;
     15import javax.annotation.Nullable;
    1416import javax.swing.JCheckBox;
    1517import javax.swing.JPanel;
    1618
     
    2628import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
    2729import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
    2830import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     31import org.openstreetmap.josm.spi.preferences.Config;
    2932import org.openstreetmap.josm.tools.GBC;
    3033import org.openstreetmap.josm.tools.Logging;
    3134import org.openstreetmap.josm.tools.Stopwatch;
     
    351354     * @param p The primitive to be tested
    352355     * @return True if building key is set and different from no,entrance
    353356     */
    354     protected static final boolean isBuilding(OsmPrimitive p) {
     357    protected static boolean isBuilding(OsmPrimitive p) {
    355358        return p.hasTagDifferent("building", "no", "entrance");
    356359    }
    357360
     
    360363     * @param p The primitive to be tested
    361364     * @return True if landuse key is equal to residential
    362365     */
    363     protected static final boolean isResidentialArea(OsmPrimitive p) {
     366    protected static boolean isResidentialArea(OsmPrimitive p) {
    364367        return p.hasTag("landuse", "residential");
    365368    }
    366369
     370    /**
     371     * Get the wiki link for a specified key and value
     372     * @param key The key
     373     * @param value The value
     374     * @return The wiki link
     375     * @since xxx
     376     */
     377    @Nonnull
     378    public static String getWikiLink(@Nonnull String key, @Nullable String value) {
     379        final String wiki = Config.getUrls().getOSMWiki() + "/wiki/";
     380        if (value == null) {
     381            return wiki + "Key:" + key;
     382        }
     383        return wiki + "Tag:" + key + "=" + value;
     384    }
     385
    367386    /**
    368387     * Free resources.
    369388     */
  • src/org/openstreetmap/josm/data/validation/TestError.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/TestError.java b/src/org/openstreetmap/josm/data/validation/TestError.java
    a b  
    1212import java.util.Locale;
    1313import java.util.TreeSet;
    1414import java.util.function.Supplier;
     15import java.util.regex.Pattern;
    1516import java.util.stream.Collectors;
    1617import java.util.stream.Stream;
    1718
     
    3334 * @since 3669
    3435 */
    3536public class TestError implements Comparable<TestError> {
     37    /** The pattern for html tags */
     38    private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\".*?\">|</a>");
    3639    /** is this error on the ignore list */
    3740    private boolean ignored;
    3841    /** Severity */
     
    350353    public String getIgnoreSubGroup() {
    351354        if (code == 3000) {
    352355            // see #19053
    353             return "3000_" + (description == null ? message : description);
     356            return "3000_" + removeHtmlTags(getDescription() == null ? getMessage() : getDescription());
    354357        }
    355358        String ignorestring = getIgnoreGroup();
    356359        if (descriptionEn != null) {
    357             ignorestring += '_' + descriptionEn;
     360            ignorestring += '_' + removeHtmlTags(descriptionEn);
    358361        }
    359362        return ignorestring;
    360363    }
     
    367370    public String getIgnoreGroup() {
    368371        if (code == 3000) {
    369372            // see #19053
    370             return "3000_" + getMessage();
     373            return "3000_" + removeHtmlTags(getMessage());
    371374        }
    372375        return Integer.toString(code);
    373376    }
     
    573576        return "TestError [tester=" + tester + ", code=" + code + ", message=" + message + ']';
    574577    }
    575578
     579    /**
     580     * Remove html tags
     581     * @param string The string to remove tags from (specifically the anchor tag)
     582     * @return The stripped string
     583     */
     584    private static String removeHtmlTags(String string) {
     585        return HTML_LINK_PATTERN.matcher(string).replaceAll("");
     586    }
    576587}
  • src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java b/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreeRenderer.java
    a b  
    33
    44import java.awt.Component;
    55
     6import javax.swing.JLabel;
    67import javax.swing.JTree;
    78import javax.swing.tree.DefaultMutableTreeNode;
    8 import javax.swing.tree.DefaultTreeCellRenderer;
    99
    1010import org.openstreetmap.josm.data.validation.Severity;
    1111import org.openstreetmap.josm.data.validation.TestError;
    1212import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
     13import org.openstreetmap.josm.gui.widgets.HtmlTreeCellRenderer;
    1314import org.openstreetmap.josm.tools.ImageProvider;
    1415
    1516/**
    1617 * Tree renderer for displaying errors
    1718 * @author frsantos
    1819 */
    19 public class ValidatorTreeRenderer extends DefaultTreeCellRenderer {
     20public class ValidatorTreeRenderer extends HtmlTreeCellRenderer {
     21
     22    private static final long serialVersionUID = 4750085115702320153L;
    2023
    2124    @Override
    2225    public Component getTreeCellRendererComponent(JTree tree, Object value,
    2326            boolean selected, boolean expanded, boolean leaf, int row,
    2427            boolean hasFocus) {
    25         super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
     28        final Component component = super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
    2629
    27         DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
    28         Object nodeInfo = node.getUserObject();
     30        final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
     31        final Object nodeInfo = node.getUserObject();
    2932
    30         if (nodeInfo instanceof Severity) {
    31             Severity s = (Severity) nodeInfo;
    32             setIcon(ImageProvider.get("data", s.getIcon()));
    33         } else if (nodeInfo instanceof TestError) {
    34             TestError error = (TestError) nodeInfo;
    35             MultipleNameVisitor v = error.getNameVisitor();
    36             setText(v.getText());
    37             setIcon(v.getIcon());
     33        if (component instanceof JLabel) {
     34            final JLabel label = (JLabel) component;
     35            if (nodeInfo instanceof Severity) {
     36                final Severity s = (Severity) nodeInfo;
     37                label.setIcon(ImageProvider.get("data", s.getIcon()));
     38            } else if (nodeInfo instanceof TestError) {
     39                final TestError error = (TestError) nodeInfo;
     40                MultipleNameVisitor v = error.getNameVisitor();
     41                label.setText(v.getText());
     42                label.setIcon(v.getIcon());
     43            }
    3844        }
    39         return this;
     45        return component;
    4046    }
    4147}
  • new file src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java b/src/org/openstreetmap/josm/gui/widgets/HtmlTreeCellRenderer.java
    new file mode 100644
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui.widgets;
     3
     4import java.awt.Color;
     5import java.awt.Component;
     6import java.awt.Cursor;
     7import java.awt.Point;
     8import java.awt.Rectangle;
     9import java.awt.event.MouseEvent;
     10import java.text.MessageFormat;
     11import java.util.Arrays;
     12
     13import javax.annotation.Nullable;
     14import javax.swing.JLabel;
     15import javax.swing.JTree;
     16import javax.swing.event.HyperlinkEvent;
     17import javax.swing.event.MouseInputListener;
     18import javax.swing.text.html.HTMLDocument;
     19import javax.swing.text.html.StyleSheet;
     20import javax.swing.tree.DefaultMutableTreeNode;
     21import javax.swing.tree.DefaultTreeCellRenderer;
     22import javax.swing.tree.TreePath;
     23
     24import org.openstreetmap.josm.tools.ColorHelper;
     25import org.openstreetmap.josm.tools.OpenBrowser;
     26
     27/**
     28 * A {@link javax.swing.tree.TreeCellRenderer} for cells that may contain hyperlinks.
     29 * @since xxx
     30 */
     31public class HtmlTreeCellRenderer extends DefaultTreeCellRenderer {
     32    /**
     33     * This only exists since the JTree cannot pass events to subcomponents.
     34     */
     35    private static class JTreeMouseInputListener implements MouseInputListener {
     36        @Override
     37        public void mouseClicked(MouseEvent mouseEvent) {
     38            dispatchEvent(mouseEvent);
     39        }
     40
     41        @Override
     42        public void mouseMoved(MouseEvent mouseEvent) {
     43            dispatchEvent(mouseEvent);
     44        }
     45
     46        @Override
     47        public void mousePressed(MouseEvent mouseEvent) {
     48            // Do nothing -- this can cause issues with the popup menu
     49        }
     50
     51        @Override
     52        public void mouseReleased(MouseEvent mouseEvent) {
     53            // Do nothing -- this can cause issues with the popup menu
     54        }
     55
     56        @Override
     57        public void mouseEntered(MouseEvent mouseEvent) {
     58            dispatchEvent(mouseEvent);
     59        }
     60
     61        @Override
     62        public void mouseExited(MouseEvent mouseEvent) {
     63            dispatchEvent(mouseEvent);
     64        }
     65
     66        @Override
     67        public void mouseDragged(MouseEvent mouseEvent) {
     68            dispatchEvent(mouseEvent);
     69        }
     70
     71        /**
     72         * Dispatch mouse events for HTML-like cells
     73         * @param mouseEvent The event to dispatch
     74         */
     75        private static void dispatchEvent(MouseEvent mouseEvent) {
     76            if (mouseEvent.getComponent() instanceof JTree) {
     77                final Point p = mouseEvent.getPoint();
     78                final JTree tree = (JTree) mouseEvent.getComponent();
     79                final Component component = getComponentAt(tree, p); // p is translated here
     80                if (component != null) {
     81                    component.dispatchEvent(new MouseEvent(component, // Use the component we found to fire the event
     82                            mouseEvent.getID(), mouseEvent.getWhen(), mouseEvent.getModifiers(),
     83                            p.x, p.y, mouseEvent.getClickCount(), mouseEvent.isPopupTrigger(),
     84                            mouseEvent.getButton()));
     85                    component.setBounds(0, 0, 0, 0);
     86                }
     87            }
     88        }
     89
     90        /**
     91         * Get a rendered component for HTML
     92         * @param tree The tree to get the component from
     93         * @param p The point to get the component at (will be modified)
     94         * @return The component
     95         */
     96        @Nullable
     97        private static Component getComponentAt(JTree tree, Point p) {
     98            final TreePath path = tree.getPathForLocation(p.x, p.y);
     99            if (path != null && tree.getCellRenderer() instanceof HtmlTreeCellRenderer) {
     100                final HtmlTreeCellRenderer renderer = (HtmlTreeCellRenderer) tree.getCellRenderer();
     101                final JosmEditorPane panel = renderer.panel;
     102                renderer.panel.setText(null); // Reset text in panel to avoid having links in sub-elements.
     103                final int row = tree.getRowForPath(path);
     104                final Object last = path.getLastPathComponent();
     105                // We need to get the right text into the panel, so call the renderer
     106                final Component component = renderer.getTreeCellRendererComponent(tree, last,
     107                        tree.isRowSelected(row), tree.isExpanded(row),
     108                        tree.getModel().isLeaf(last), row, true);
     109                final Rectangle bounds = tree.getPathBounds(path);
     110                if (bounds != null) {
     111                    // If we don't translate the point, we are attempting to get the link in the wrong frame of reference
     112                    // The reference x/y are 0. This is set in BasicTextUI#getVisibleEditorRect, so we must translate the point here.
     113                    final int width = (component instanceof JLabel)
     114                            ? ((JLabel) component).getIcon().getIconWidth() + ((JLabel) component).getIconTextGap()
     115                            : 0;
     116                    // This translation puts the origin point at the upper-left of the JLabel, but moves it to the right of the icon to the text
     117                    // Yes, it is - width, not + width. If you change this, make certain that clicking links still works!
     118                    p.translate(-bounds.x - width, -bounds.y);
     119                    // Just make certain that we are consistent for other objects.
     120                    bounds.x = 0;
     121                    bounds.y = 0;
     122                    // Set the correct width
     123                    bounds.width -= width;
     124                    // Set the bounds from the JTree component (needed for proper layout)
     125                    panel.setBounds(bounds);
     126                    return panel;
     127                }
     128            }
     129            return null;
     130        }
     131    }
     132
     133    private static final long serialVersionUID = -4842755204541968238L;
     134    /**
     135     * A reusable panel to avoid new objects where possible.
     136     * It isn't worth it to make a new object for each row, since it doesn't receive mouse events (tested on Java 8).
     137     */
     138    private final JosmEditorPane panel = new JosmEditorPane();
     139    /** JTree does not send mouse events to subcomponents */
     140    private final JTreeMouseInputListener mouseTreeListener = new JTreeMouseInputListener();
     141    /** The tree used to set cursors when entering/leaving a hyperlink */
     142    private JTree tree;
     143
     144    /**
     145     * Create a new renderer
     146     */
     147    public HtmlTreeCellRenderer() {
     148        panel.addHyperlinkListener(e -> {
     149            if (e.getURL() != null) {
     150                if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
     151                    OpenBrowser.displayUrl(e.getURL().toString());
     152                } else if (this.tree != null && HyperlinkEvent.EventType.ENTERED.equals(e.getEventType())) {
     153                    this.tree.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
     154                } else if (this.tree != null && HyperlinkEvent.EventType.EXITED.equals(e.getEventType())) {
     155                    this.tree.setCursor(Cursor.getDefaultCursor());
     156                }
     157            }
     158        });
     159        JosmEditorPane.makeJLabelLike(panel, false);
     160        final Color defaultLink = ColorHelper.html2color(JosmEditorPane.getLinkColor());
     161        final Color lighterLink = defaultLink.brighter().brighter();
     162        final Color darkerLink = defaultLink.darker().darker();
     163        final String divRule = "{0} '{'background-color:{1}; color:{2};'}'";
     164        final String aRule = "{0} a '{'text-decoration: underline; color:{1}'}'";
     165        StyleSheet ss = ((HTMLDocument) panel.getDocument()).getStyleSheet();
     166        ss.addRule(MessageFormat.format(divRule, ".selected", ColorHelper.color2html(getBackgroundSelectionColor()),
     167                ColorHelper.color2html(getTextSelectionColor())));
     168        ss.addRule(MessageFormat.format(aRule, ".selected",
     169                ColorHelper.color2html(getAppropriateColor(getBackgroundSelectionColor(), defaultLink, lighterLink, darkerLink))));
     170        ss.addRule(MessageFormat.format(divRule, ".not-selected", ColorHelper.color2html(getBackgroundNonSelectionColor()),
     171                ColorHelper.color2html(getTextNonSelectionColor())));
     172        ss.addRule(MessageFormat.format(aRule, ".not-selected",
     173                ColorHelper.color2html(getAppropriateColor(getBackgroundNonSelectionColor(), defaultLink, lighterLink, darkerLink))));
     174    }
     175
     176    @Override
     177    public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded,
     178                                                  boolean leaf, int row, boolean hasFocus) {
     179        final Component component = super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
     180        this.tree = tree;
     181        if (value instanceof DefaultMutableTreeNode) {
     182            if (Arrays.stream(tree.getMouseListeners()).noneMatch(this.mouseTreeListener::equals)) {
     183                tree.addMouseListener(this.mouseTreeListener);
     184                tree.addMouseMotionListener(this.mouseTreeListener);
     185            }
     186            final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
     187            final Object object = node.getUserObject();
     188            if (object instanceof String) {
     189                final String text = (String) object;
     190                final String modified = "<html><div class=\"" + (sel ? "selected" : "not-selected") + "\">"
     191                        + text + "</div></html>";
     192                this.panel.setText(modified);
     193                this.setText(this.panel.getText()); // This gets the custom CSS into the JLabel
     194            }
     195        }
     196        return component;
     197    }
     198
     199    /**
     200     * Convert the rgb value for use in a relative luminance calculation
     201     * @param rgb The value to convert
     202     * @return The appropriate value to use in the calculation
     203     */
     204    private static double convertRGB(int rgb) {
     205        final double normalized = rgb / 255d;
     206        if (normalized <= 0.03928) {
     207            return normalized / 12.92;
     208        }
     209        return Math.pow(((normalized + 0.055)/1.055), 2.4);
     210    }
     211
     212    /**
     213     * Calculate the contrast ratio between two luminance values
     214     * @param luminanceOne The first luminance value
     215     * @param luminanceTwo The second luminance value
     216     * @return The contrast ratio. Higher is usually better.
     217     */
     218    private static double contrastRatio(double luminanceOne, double luminanceTwo) {
     219        final double min = Math.min(luminanceOne, luminanceTwo);
     220        final double max = Math.max(luminanceOne, luminanceTwo);
     221        return (max + 0.05) / (min + 0.05);
     222    }
     223
     224    /**
     225     * Calculate the luminance of a color
     226     * @param red The red part
     227     * @param green The green part
     228     * @param blue The blue part
     229     * @return The relative luminance (0-1, 0 black, 1 white)
     230     */
     231    private static double luminance(int red, int green, int blue) {
     232        return 0.2126 * convertRGB(red)
     233                + 0.7152 * convertRGB(green)
     234                + 0.0722 * convertRGB(blue);
     235    }
     236
     237    /**
     238     * Get the appropriate color given a background
     239     * @param background The background color
     240     * @param options The options to choose from
     241     * @return The appropriate color
     242     */
     243    private static Color getAppropriateColor(Color background, Color... options) {
     244        if (options.length < 2) {
     245            throw new IllegalArgumentException("There must be at least two color options");
     246        }
     247        // sRGB calculation
     248        final double backgroundLuminance = luminance(background.getRed(), background.getGreen(), background.getBlue());
     249        double lastContrastRatio = 0; // W3 recommends a contrast ratio of at least 4.5:1. We should be shooting for 7:1.
     250        Color lastColor = null;
     251        for (Color option : options) {
     252            final double contrastRatio = contrastRatio(backgroundLuminance, luminance(option.getRed(), option.getGreen(), option.getBlue()));
     253            if (contrastRatio > lastContrastRatio) {
     254                lastContrastRatio = contrastRatio;
     255                lastColor = option;
     256            }
     257        }
     258        return lastColor;
     259    }
     260}
  • test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/MapCSSTagCheckerTest.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.validation.tests;
    33
     4import static org.junit.jupiter.api.Assertions.assertAll;
    45import static org.junit.jupiter.api.Assertions.assertEquals;
    56import static org.junit.jupiter.api.Assertions.assertFalse;
    67import static org.junit.jupiter.api.Assertions.assertNotNull;
     
    1415import java.util.LinkedHashSet;
    1516import java.util.List;
    1617import java.util.Set;
     18import java.util.stream.Stream;
    1719
    1820import org.junit.jupiter.api.BeforeEach;
    1921import org.junit.jupiter.api.Test;
    2022import org.junit.jupiter.api.extension.RegisterExtension;
     23import org.junit.jupiter.params.ParameterizedTest;
     24import org.junit.jupiter.params.provider.Arguments;
     25import org.junit.jupiter.params.provider.MethodSource;
    2126import org.openstreetmap.josm.TestUtils;
    2227import org.openstreetmap.josm.command.ChangePropertyCommand;
    2328import org.openstreetmap.josm.command.ChangePropertyKeyCommand;
     
    105110        final Collection<TestError> errors = check.getErrorsForPrimitive(n1, check.whichSelectorMatchesPrimitive(n1), new Environment(), null);
    106111        assertEquals(1, errors.size());
    107112        TestError err = errors.iterator().next();
    108         assertEquals("deprecated", err.getMessage());
    109         assertEquals("natural=marsh is deprecated", err.getDescription());
    110         assertEquals(Severity.WARNING, err.getSeverity());
    111         assertEquals("Sequence: Fix of natural=marsh is deprecated", check.fixPrimitive(n1).getDescriptionText());
    112         assertEquals("{natural=}", ((ChangePropertyCommand) check.fixPrimitive(n1).getChildren().iterator().next()).getTags().toString());
    113         assertFalse(check.test(n2));
    114         assertEquals("The key is natural and the value is marsh",
    115                 MapCSSTagCheckerRule.insertArguments(check.rule.selectors.get(0), "The key is {0.key} and the value is {0.value}", null));
     113        assertAll(() -> assertEquals("deprecated", err.getMessage()),
     114                () -> assertEquals("<a href=\"https://wiki.openstreetmap.org/wiki/Key:natural\">natural</a>=marsh is deprecated",
     115                        err.getDescription()),
     116                () -> assertEquals(Severity.WARNING, err.getSeverity()),
     117                () -> assertEquals("Sequence: Fix of natural=marsh is deprecated", check.fixPrimitive(n1).getDescriptionText()),
     118                () -> assertEquals("{natural=}",
     119                        ((ChangePropertyCommand) check.fixPrimitive(n1).getChildren().iterator().next()).getTags().toString()),
     120                () -> assertFalse(check.test(n2)),
     121                () -> assertEquals("The key is natural and the value is marsh",
     122                MapCSSTagCheckerRule.insertArguments(check.rule.selectors.get(0),
     123                        "The key is {0.key} and the value is {0.value}", null, false)));
    116124    }
    117125
    118126    /**
     
    144152                "throwWarning: tr(\"has {0} but not {1}\", \"{0.key}\", \"{1.key}\");}");
    145153        final OsmPrimitive p = OsmUtils.createPrimitive("way alt_name=Foo");
    146154        final Collection<TestError> errors = test.getErrorsForPrimitive(p, false);
    147         assertEquals(1, errors.size());
    148         assertEquals("has alt_name but not name", errors.iterator().next().getMessage());
    149         assertEquals("3000_has alt_name but not name", errors.iterator().next().getIgnoreSubGroup());
     155        assertAll(() -> assertEquals(1, errors.size()),
     156                () -> assertEquals("has <a href=\"https://wiki.openstreetmap.org/wiki/Key:alt_name\">alt_name</a> but not " +
     157                        "<a href=\"https://wiki.openstreetmap.org/wiki/Key:name\">name</a>", errors.iterator().next().getMessage()),
     158                () -> assertEquals("3000_has alt_name but not name", errors.iterator().next().getIgnoreSubGroup()));
    150159    }
    151160
    152161    /**
     
    159168                "  throwWarning: tr(\"{0} used with {1}\", \"{0.value}\", \"{1.tag}\");}");
    160169        final OsmPrimitive p = OsmUtils.createPrimitive("way highway=footway foot=no");
    161170        final Collection<TestError> errors = test.getErrorsForPrimitive(p, false);
    162         assertEquals(1, errors.size());
    163         assertEquals("footway used with foot=no", errors.iterator().next().getMessage());
    164         assertEquals("3000_footway used with foot=no", errors.iterator().next().getIgnoreSubGroup());
     171        assertAll(() -> assertEquals(1, errors.size()),
     172                () -> assertEquals("<a href=\"https://wiki.openstreetmap.org/wiki/Tag:highway=footway\">footway</a> used with " +
     173                        "<a href=\"https://wiki.openstreetmap.org/wiki/Tag:foot=no\">foot=no</a>", errors.iterator().next().getMessage()),
     174                () -> assertEquals("3000_footway used with foot=no", errors.iterator().next().getIgnoreSubGroup()));
    165175    }
    166176
    167177    /**
     
    207217        assertTrue(c.getErrorsForPrimitive(node, false).isEmpty());
    208218    }
    209219
     220    static Stream<Arguments> testAssertions() {
     221        MapCSSTagChecker c = new MapCSSTagChecker();
     222        return ValidatorPrefHelper.INSTANCE.getDefault().stream()
     223                .map(entry -> Arguments.of(entry, c));
     224    }
     225
    210226    /**
    211227     * Unit test for all {@link TagTest} assertions.
    212228     * @throws Exception if an error occurs
    213229     */
    214     @Test
    215     void testAssertions() throws Exception {
    216         MapCSSTagChecker c = new MapCSSTagChecker();
     230    @ParameterizedTest
     231    @MethodSource
     232    void testAssertions(ExtendedSourceEntry entry, MapCSSTagChecker c) throws Exception {
    217233        Set<String> assertionErrors = new LinkedHashSet<>();
    218234
    219235        // initialize
    220         for (ExtendedSourceEntry entry : ValidatorPrefHelper.INSTANCE.getDefault()) {
    221             c.addMapCSS(entry.url, assertionErrors::add);
    222         }
     236        c.addMapCSS(entry.url, assertionErrors::add);
    223237
    224238        for (String msg : assertionErrors) {
    225239            Logging.error(msg);
  • test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java b/test/unit/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetValidationTest.java
    a b  
    44import static org.junit.jupiter.api.Assertions.assertEquals;
    55import static org.junit.jupiter.api.Assertions.assertTrue;
    66
    7 import java.util.Arrays;
     7import java.util.Collections;
    88import java.util.Locale;
    99
    1010import javax.swing.JLabel;
     
    3131     */
    3232    @RegisterExtension
    3333    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    34     public JOSMTestRules rule = new JOSMTestRules().projection();
     34    static JOSMTestRules rule = new JOSMTestRules().projection();
    3535
    3636    @BeforeEach
    3737    void setUp() {
     
    5454        assertTrue(label.isVisible());
    5555        assertEquals("<html><ul>" +
    5656            "<li>Opening hours syntax (Hours without minutes)</li>" +
    57             "<li>unusual value of width: meters is default; only positive values; point is decimal separator; if units, put space then unit</li>" +
    58             "<li>unusual value of incline, use x% or x° or up or down instead</li>" +
    59             "<li>suspicious tag combination (width on suspicious object)</li>" +
    60             "<li>suspicious tag combination (incline on suspicious object)</li></ul>", label.getToolTipText());
     57            "<li>unusual value of <a href=\"https://wiki.openstreetmap.org/wiki/Key:width\">width</a>: meters is default; only positive values; point is decimal separator; if units, put space then unit</li>" +
     58            "<li>unusual value of <a href=\"https://wiki.openstreetmap.org/wiki/Key:incline\">incline</a>, use x% or x° or up or down instead</li>" +
     59            "<li>suspicious tag combination (<a href=\"https://wiki.openstreetmap.org/wiki/Key:width\">width</a> on suspicious object)</li>" +
     60            "<li>suspicious tag combination (<a href=\"https://wiki.openstreetmap.org/wiki/Key:incline\">incline</a> on suspicious object)</li></ul>", label.getToolTipText());
    6161        // CHECKSTYLE.ON: LineLength
    6262    }
    6363
     
    6868    void testApplyChangedTags() {
    6969        OsmPrimitive primitive = OsmUtils.createPrimitive("way incline=10m width=1mm opening_hours=\"Mo-Fr 8-10\"");
    7070        new DataSet(primitive);
    71         OsmPrimitive clone = TaggingPresetValidation.applyChangedTags(primitive, Arrays.asList(new Tag("incline", "20m")));
     71        OsmPrimitive clone = TaggingPresetValidation.applyChangedTags(primitive, Collections.singletonList(new Tag("incline", "20m")));
    7272        assertEquals("20m", clone.get("incline"));
    7373        assertEquals("1mm", clone.get("width"));
    7474    }