Ticket #22677: 22677.patch
| File 22677.patch, 15.7 KB (added by , 3 years ago) |
|---|
-
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 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 6 import java.awt.BorderLayout; 7 import java.awt.Color; 7 8 import java.awt.Component; 8 9 import java.awt.Dimension; 9 10 import java.awt.event.ActionEvent; … … 11 12 import java.util.Collection; 12 13 import java.util.Collections; 13 14 import java.util.EnumSet; 15 import java.util.HashMap; 14 16 import java.util.HashSet; 15 17 import java.util.Iterator; 16 18 import java.util.List; 17 19 import java.util.Locale; 20 import java.util.Map; 18 21 import java.util.Objects; 19 22 import java.util.Set; 23 import java.util.function.Predicate; 20 24 import java.util.regex.Pattern; 21 25 import java.util.stream.Collectors; 22 26 … … 43 47 import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect; 44 48 import org.openstreetmap.josm.gui.tagging.presets.items.Key; 45 49 import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem; 50 import org.openstreetmap.josm.gui.tagging.presets.items.Label; 46 51 import org.openstreetmap.josm.gui.tagging.presets.items.Roles; 47 52 import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role; 48 53 import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 49 54 import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel; 55 import org.openstreetmap.josm.tools.ColorHelper; 50 56 import org.openstreetmap.josm.tools.Destroyable; 51 57 import org.openstreetmap.josm.tools.Utils; 52 58 … … 63 69 private static final int CLASSIFICATION_TAGS_MATCH = 100; 64 70 65 71 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); 66 74 67 75 private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true); 68 76 private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true); … … 73 81 private boolean typesInSelectionDirty = true; 74 82 private final transient PresetClassifications classifications = new PresetClassifications(); 75 83 76 private staticclass ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {84 private class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> { 77 85 private final DefaultListCellRenderer def = new DefaultListCellRenderer(); 78 86 @Override 79 87 public Component getListCellRendererComponent(JList<? extends TaggingPreset> list, TaggingPreset tp, int index, 80 88 boolean isSelected, boolean cellHasFocus) { 81 89 JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus); 90 result.setToolTipText(null); 82 91 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() + " <small style=\"color:" + col + "\">" 109 + matchingTags + "</small></html>"); 110 } 111 } 112 } 83 113 result.setIcon((Icon) tp.getValue(Action.SMALL_ICON)); 84 114 return result; 85 115 } 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 } 86 171 } 87 172 88 173 /** 89 174 * Computes the match ration of a {@link TaggingPreset} wrt. a searchString. 90 175 */ 91 176 public static class PresetClassification implements Comparable<PresetClassification> { 177 /** The preset for this {@link PresetClassification} */ 92 178 public final TaggingPreset preset; 179 /** Used for sorting presets that match a query */ 93 180 public int classification; 181 /** Used to indicate that this was used last by a user (for "history") */ 94 182 public int favoriteIndex; 95 183 private final Collection<String> groups; 96 184 private final Collection<String> names; … … 100 188 this.preset = preset; 101 189 Set<String> groupSet = new HashSet<>(); 102 190 Set<String> nameSet = new HashSet<>(); 103 Set<String> tagSet = new HashSet<>();104 191 TaggingPreset group = preset.group; 105 192 while (group != null) { 106 193 addLocaleNames(groupSet, group); 107 194 group = group.group; 108 195 } 109 196 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 128 201 this.groups = Utils.toUnmodifiableList(groupSet); 129 202 this.names = Utils.toUnmodifiableList(nameSet); 130 203 this.tags = Utils.toUnmodifiableList(tagSet); … … 133 206 private static void addLocaleNames(Collection<String> collection, TaggingPreset preset) { 134 207 String locName = preset.getLocaleName(); 135 208 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)); 137 210 } 138 211 } 139 212 … … 189 262 return result; 190 263 } 191 264 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 192 277 @Override 193 278 public String toString() { 194 279 return Integer.toString(classification) + ' ' + preset; 195 280 } 196 281 } 197 282 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 198 311 /** 199 312 * Constructs a new {@code TaggingPresetSelector}. 200 313 * @param displayOnlyApplicable if {@code true} display "Show only applicable to selection" checkbox … … 256 369 boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected(); 257 370 258 371 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(); 260 373 final List<PresetClassification> result = classifications.getMatchingPresets( 261 374 text, onlyApplicable, inTags, getTypesInSelection(), selected); 262 375 … … 279 392 280 393 private final List<PresetClassification> classifications = new ArrayList<>(); 281 394 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 */ 282 404 public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags, 283 405 Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) { 284 406 final String[] groupWords; 285 407 final String[] nameWords; 286 408 287 409 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); 290 412 } else { 291 413 groupWords = null; 292 nameWords = searchText.split("\\s", -1);414 nameWords = PATTERN_WORD.split(searchText, -1); 293 415 } 294 416 295 417 return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives); 296 418 } 297 419 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 */ 298 430 public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable, 299 431 boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) { 300 432
