Ticket #22677: 22677.patch

File 22677.patch, 15.7 KB (added by taylor.smock, 3 years ago)

Indicate what tags from a preset are causing the preset to be found when using the Search presets window

  • src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java

    Subject: [PATCH] See #22677: remove man_made=communications_tower
    
    The root problem is that people are (apparently) doing a search for communication
    and selecting the entry with communication in the name.
    ---
    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java b/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java
    a b  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.awt.BorderLayout;
     7import java.awt.Color;
    78import java.awt.Component;
    89import java.awt.Dimension;
    910import java.awt.event.ActionEvent;
     
    1112import java.util.Collection;
    1213import java.util.Collections;
    1314import java.util.EnumSet;
     15import java.util.HashMap;
    1416import java.util.HashSet;
    1517import java.util.Iterator;
    1618import java.util.List;
    1719import java.util.Locale;
     20import java.util.Map;
    1821import java.util.Objects;
    1922import java.util.Set;
     23import java.util.function.Predicate;
    2024import java.util.regex.Pattern;
    2125import java.util.stream.Collectors;
    2226
     
    4347import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
    4448import org.openstreetmap.josm.gui.tagging.presets.items.Key;
    4549import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
     50import org.openstreetmap.josm.gui.tagging.presets.items.Label;
    4651import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
    4752import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
    4853import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
    4954import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
     55import org.openstreetmap.josm.tools.ColorHelper;
    5056import org.openstreetmap.josm.tools.Destroyable;
    5157import org.openstreetmap.josm.tools.Utils;
    5258
     
    6369    private static final int CLASSIFICATION_TAGS_MATCH = 100;
    6470
    6571    private static final Pattern PATTERN_PUNCTUATION = Pattern.compile("\\p{Punct}", Pattern.UNICODE_CHARACTER_CLASS);
     72    private static final Pattern PATTERN_WORD = Pattern.compile("\\s", Pattern.UNICODE_CHARACTER_CLASS);
     73    private static final Pattern PATTERN_GROUP = Pattern.compile("[\\s/]", Pattern.UNICODE_CHARACTER_CLASS);
    6674
    6775    private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
    6876    private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
     
    7381    private boolean typesInSelectionDirty = true;
    7482    private final transient PresetClassifications classifications = new PresetClassifications();
    7583
    76     private static class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
     84    private class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
    7785        private final DefaultListCellRenderer def = new DefaultListCellRenderer();
    7886        @Override
    7987        public Component getListCellRendererComponent(JList<? extends TaggingPreset> list, TaggingPreset tp, int index,
    8088                boolean isSelected, boolean cellHasFocus) {
    8189            JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus);
     90            result.setToolTipText(null);
    8291            result.setText(tp.getName());
     92            if (list.getModel().getSize() < 200 // Maybe this should be configurable?
     93                    && TaggingPresetSelector.this.ckSearchInTags != null
     94                    && TaggingPresetSelector.this.ckSearchInTags.isSelected()) {
     95                final String searchText = getSearchText();
     96                final PresetClassification classification = new PresetClassification(tp);
     97                final String[] wordSet = PATTERN_WORD.split(searchText, -1);
     98                if (classification.isMatchingTags(wordSet) > 0) {
     99                    final String matchingTags = buildMatchingTagText(getSearchableTags(tp), wordSet);
     100                    if (matchingTags.length() > 0) {
     101                        result.setToolTipText(tr("Matching tags: {0}", matchingTags));
     102                        Color complement = ColorHelper.complement(result.getBackground());
     103                        // This gray works for light/dark themes
     104                        if (result.getBackground().getRGB() == Color.WHITE.getRGB()) {
     105                            complement = Color.GRAY;
     106                        }
     107                        String col = ColorHelper.color2html(complement);
     108                        result.setText("<html>" + tp.getName() + "&nbsp;&nbsp;&nbsp;&nbsp;<small style=\"color:" + col + "\">"
     109                        + matchingTags + "</small></html>");
     110                    }
     111                }
     112            }
    83113            result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
    84114            return result;
    85115        }
     116
     117        /**
     118         * Get the text to show a user for tags that match the given search string
     119         * @param tagMap The map of searchable tags
     120         * @param wordSet The words to filter on
     121         * @return The string to show the user
     122         */
     123        private String buildMatchingTagText(Map<String, List<String>> tagMap, String[] wordSet) {
     124            final Predicate<String> matchesWords = word -> {
     125                for (String checkWord : wordSet) {
     126                    if (word.contains(checkWord)) {
     127                        return true;
     128                    }
     129                }
     130                return false;
     131            };
     132            final StringBuilder sb = new StringBuilder();
     133            for (Map.Entry<String, List<String>> entry : tagMap.entrySet()) {
     134                if (sb.length() > 0 && sb.lastIndexOf(" ") != sb.length() - 1) {
     135                    sb.append(' ');
     136                }
     137                int length = sb.length();
     138                // Add the entry if it matches
     139                if (matchesWords.test(entry.getKey())) {
     140                    sb.append(entry.getKey());
     141                    if (entry.getValue().size() == 1) {
     142                        sb.append('=').append(entry.getValue().get(0));
     143                    }
     144                }
     145
     146                if (!entry.getValue().isEmpty() && (length == sb.length() || entry.getValue().size() > 1)) {
     147                    // Add values if they match
     148                    boolean added = false;
     149                    for (String value : entry.getValue()) {
     150                        if (matchesWords.test(value)) {
     151                            // Add the key if it hasn't been added already
     152                            if (!added) {
     153                                if (length == sb.length()) {
     154                                    sb.append(entry.getKey()).append('=');
     155                                } else if (length + entry.getKey().length() == sb.length()) {
     156                                    sb.append('=');
     157                                }
     158                            }
     159                            if (added) {
     160                                sb.append(';');
     161                            } else {
     162                                added = true;
     163                            }
     164                            sb.append(value);
     165                        }
     166                    }
     167                }
     168            }
     169            return sb.toString();
     170        }
    86171    }
    87172
    88173    /**
    89174     * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
    90175     */
    91176    public static class PresetClassification implements Comparable<PresetClassification> {
     177        /** The preset for this {@link PresetClassification} */
    92178        public final TaggingPreset preset;
     179        /** Used for sorting presets that match a query */
    93180        public int classification;
     181        /** Used to indicate that this was used last by a user (for "history") */
    94182        public int favoriteIndex;
    95183        private final Collection<String> groups;
    96184        private final Collection<String> names;
     
    100188            this.preset = preset;
    101189            Set<String> groupSet = new HashSet<>();
    102190            Set<String> nameSet = new HashSet<>();
    103             Set<String> tagSet = new HashSet<>();
    104191            TaggingPreset group = preset.group;
    105192            while (group != null) {
    106193                addLocaleNames(groupSet, group);
    107194                group = group.group;
    108195            }
    109196            addLocaleNames(nameSet, preset);
    110             for (TaggingPresetItem item: preset.data) {
    111                 if (item instanceof KeyedItem) {
    112                     tagSet.add(((KeyedItem) item).key);
    113                     if (item instanceof ComboMultiSelect) {
    114                         final ComboMultiSelect cms = (ComboMultiSelect) item;
    115                         if (cms.values_searchable) {
    116                             tagSet.addAll(cms.getDisplayValues());
    117                         }
    118                     }
    119                     if (item instanceof Key && ((Key) item).value != null) {
    120                         tagSet.add(((Key) item).value);
    121                     }
    122                 } else if (item instanceof Roles) {
    123                     for (Role role : ((Roles) item).roles) {
    124                         tagSet.add(role.key);
    125                     }
    126                 }
    127             }
     197            Map<String, List<String>> searchableTags = getSearchableTags(preset);
     198            Set<String> tagSet = new HashSet<>(searchableTags.keySet());
     199            searchableTags.values().forEach(tagSet::addAll);
     200
    128201            this.groups = Utils.toUnmodifiableList(groupSet);
    129202            this.names = Utils.toUnmodifiableList(nameSet);
    130203            this.tags = Utils.toUnmodifiableList(tagSet);
     
    133206        private static void addLocaleNames(Collection<String> collection, TaggingPreset preset) {
    134207            String locName = preset.getLocaleName();
    135208            if (locName != null) {
    136                 Collections.addAll(collection, locName.toLowerCase(Locale.ENGLISH).split("\\s", -1));
     209                Collections.addAll(collection, PATTERN_WORD.split(locName.toLowerCase(Locale.ENGLISH), -1));
    137210            }
    138211        }
    139212
     
    189262                return result;
    190263        }
    191264
     265        @Override
     266        public int hashCode() {
     267            return Objects.hash(this.preset);
     268        }
     269
     270        @Override
     271        public boolean equals(Object obj) {
     272            return obj != null &&
     273                    obj.getClass().equals(this.getClass()) &&
     274                    Objects.equals(((PresetClassification) obj).preset, this.preset);
     275        }
     276
    192277        @Override
    193278        public String toString() {
    194279            return Integer.toString(classification) + ' ' + preset;
    195280        }
    196281    }
    197282
     283    /**
     284     * Get the searchable tag map for a preset
     285     * @param preset The preset to parse
     286     * @return A map of key to values that are searchable
     287     */
     288    private static Map<String, List<String>> getSearchableTags(TaggingPreset preset) {
     289        final Map<String, List<String>> tagMap = new HashMap<>();
     290        for (TaggingPresetItem item: preset.data) {
     291            if (item instanceof KeyedItem) {
     292                Collection<String> list = tagMap.computeIfAbsent(((KeyedItem) item).key, k -> new ArrayList<>());
     293                if (item instanceof ComboMultiSelect) {
     294                    final ComboMultiSelect cms = (ComboMultiSelect) item;
     295                    if (cms.values_searchable) {
     296                        list.addAll(cms.getDisplayValues());
     297                    }
     298                }
     299                if (item instanceof Key && ((Key) item).value != null) {
     300                    list.add(((Key) item).value);
     301                }
     302            } else if (item instanceof Roles) {
     303                for (Role role : ((Roles) item).roles) {
     304                    tagMap.computeIfAbsent(role.key, k -> new ArrayList<>()); // new list just in case a role is also a tag
     305                }
     306            }
     307        }
     308        return tagMap;
     309    }
     310
    198311    /**
    199312     * Constructs a new {@code TaggingPresetSelector}.
    200313     * @param displayOnlyApplicable if {@code true} display "Show only applicable to selection" checkbox
     
    256369        boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected();
    257370
    258371        DataSet ds = OsmDataManager.getInstance().getEditDataSet();
    259         Collection<OsmPrimitive> selected = (ds == null) ? Collections.<OsmPrimitive>emptyList() : ds.getSelected();
     372        Collection<OsmPrimitive> selected = (ds == null) ? Collections.emptyList() : ds.getSelected();
    260373        final List<PresetClassification> result = classifications.getMatchingPresets(
    261374                text, onlyApplicable, inTags, getTypesInSelection(), selected);
    262375
     
    279392
    280393        private final List<PresetClassification> classifications = new ArrayList<>();
    281394
     395        /**
     396         * Get the matching presets given some queries
     397         * @param searchText The search text to use. Split on {@code "/"} and whitespace.
     398         * @param onlyApplicable {@code true} if only presets that match the {@code presetTypes} or {@code selectedPrimitives} are desired
     399         * @param inTags Check if {@code nameWords} are in the tags of the preset
     400         * @param presetTypes The preset types to filter on. See {@link TaggingPreset#typeMatches(Collection)}.
     401         * @param selectedPrimitives The selected primitives to filter against (mostly for roles)
     402         * @return The list of matching preset classifications
     403         */
    282404        public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags,
    283405                Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
    284406            final String[] groupWords;
    285407            final String[] nameWords;
    286408
    287409            if (searchText.contains("/")) {
    288                 groupWords = searchText.substring(0, searchText.lastIndexOf('/')).split("[\\s/]", -1);
    289                 nameWords = searchText.substring(searchText.indexOf('/') + 1).split("\\s", -1);
     410                groupWords = PATTERN_GROUP.split(searchText.substring(0, searchText.lastIndexOf('/')), -1);
     411                nameWords = PATTERN_WORD.split(searchText.substring(searchText.indexOf('/') + 1), -1);
    290412            } else {
    291413                groupWords = null;
    292                 nameWords = searchText.split("\\s", -1);
     414                nameWords = PATTERN_WORD.split(searchText, -1);
    293415            }
    294416
    295417            return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
    296418        }
    297419
     420        /**
     421         * Get the matching presets given some queries
     422         * @param groupWords See {@link PresetClassification#isMatchingGroup(String...)} (may be {@code null}).
     423         * @param nameWords The {@link PresetClassification} is checked to see if a name, group, or tags (if {@code inTags = true}) match
     424         * @param onlyApplicable {@code true} if only presets that match the {@code presetTypes} or {@code selectedPrimitives} are desired
     425         * @param inTags Check if {@code nameWords} are in the tags of the preset
     426         * @param presetTypes The preset types to filter on. See {@link TaggingPreset#typeMatches(Collection)}.
     427         * @param selectedPrimitives The selected primitives to filter against (mostly for roles)
     428         * @return The list of matching preset classifications
     429         */
    298430        public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable,
    299431                boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
    300432