source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPresetSelector.java@ 13836

Last change on this file since 13836 was 13836, checked in by Don-vip, 6 years ago

fix #13889 - Make preset searchs ignore accents

  • 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.tagging.presets;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.event.ActionEvent;
10import java.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.EnumSet;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Locale;
18import java.util.Objects;
19import java.util.Set;
20import java.util.stream.Collectors;
21
22import javax.swing.AbstractAction;
23import javax.swing.Action;
24import javax.swing.BoxLayout;
25import javax.swing.DefaultListCellRenderer;
26import javax.swing.Icon;
27import javax.swing.JCheckBox;
28import javax.swing.JLabel;
29import javax.swing.JList;
30import javax.swing.JPanel;
31import javax.swing.JPopupMenu;
32import javax.swing.ListCellRenderer;
33import javax.swing.event.ListSelectionEvent;
34import javax.swing.event.ListSelectionListener;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.data.SelectionChangedListener;
38import org.openstreetmap.josm.data.osm.DataSet;
39import org.openstreetmap.josm.data.osm.OsmPrimitive;
40import org.openstreetmap.josm.data.preferences.BooleanProperty;
41import org.openstreetmap.josm.gui.MainApplication;
42import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
43import org.openstreetmap.josm.gui.tagging.presets.items.Key;
44import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
45import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
46import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
47import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
48import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
49import org.openstreetmap.josm.tools.Utils;
50
51/**
52 * GUI component to select tagging preset: the list with filter and two checkboxes
53 * @since 6068
54 */
55public class TaggingPresetSelector extends SearchTextResultListPanel<TaggingPreset> implements SelectionChangedListener {
56
57 private static final int CLASSIFICATION_IN_FAVORITES = 300;
58 private static final int CLASSIFICATION_NAME_MATCH = 300;
59 private static final int CLASSIFICATION_GROUP_MATCH = 200;
60 private static final int CLASSIFICATION_TAGS_MATCH = 100;
61
62 private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
63 private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
64
65 private final JCheckBox ckOnlyApplicable;
66 private final JCheckBox ckSearchInTags;
67 private final Set<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class);
68 private boolean typesInSelectionDirty = true;
69 private final transient PresetClassifications classifications = new PresetClassifications();
70
71 private static class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
72 private final DefaultListCellRenderer def = new DefaultListCellRenderer();
73 @Override
74 public Component getListCellRendererComponent(JList<? extends TaggingPreset> list, TaggingPreset tp, int index,
75 boolean isSelected, boolean cellHasFocus) {
76 JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus);
77 result.setText(tp.getName());
78 result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
79 return result;
80 }
81 }
82
83 /**
84 * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
85 */
86 public static class PresetClassification implements Comparable<PresetClassification> {
87 public final TaggingPreset preset;
88 public int classification;
89 public int favoriteIndex;
90 private final Collection<String> groups = new HashSet<>();
91 private final Collection<String> names = new HashSet<>();
92 private final Collection<String> tags = new HashSet<>();
93
94 PresetClassification(TaggingPreset preset) {
95 this.preset = preset;
96 TaggingPreset group = preset.group;
97 while (group != null) {
98 addLocaleNames(groups, group);
99 group = group.group;
100 }
101 addLocaleNames(names, preset);
102 for (TaggingPresetItem item: preset.data) {
103 if (item instanceof KeyedItem) {
104 tags.add(((KeyedItem) item).key);
105 if (item instanceof ComboMultiSelect) {
106 final ComboMultiSelect cms = (ComboMultiSelect) item;
107 if (Boolean.parseBoolean(cms.values_searchable)) {
108 tags.addAll(cms.getDisplayValues());
109 }
110 }
111 if (item instanceof Key && ((Key) item).value != null) {
112 tags.add(((Key) item).value);
113 }
114 } else if (item instanceof Roles) {
115 for (Role role : ((Roles) item).roles) {
116 tags.add(role.key);
117 }
118 }
119 }
120 }
121
122 private static void addLocaleNames(Collection<String> collection, TaggingPreset preset) {
123 String locName = preset.getLocaleName();
124 if (locName != null) {
125 Collections.addAll(collection, locName.toLowerCase(Locale.ENGLISH).split("\\s"));
126 }
127 }
128
129 private static int isMatching(Collection<String> values, String... searchString) {
130 int sum = 0;
131 List<String> deaccentedValues = values.stream().map(
132 s -> Utils.deAccent(s).toLowerCase(Locale.ENGLISH)).collect(Collectors.toList());
133 for (String word: searchString) {
134 boolean found = false;
135 boolean foundFirst = false;
136 String deaccentedWord = Utils.deAccent(word);
137 for (String value: deaccentedValues) {
138 int index = value.indexOf(deaccentedWord);
139 if (index == 0) {
140 foundFirst = true;
141 break;
142 } else if (index > 0) {
143 found = true;
144 }
145 }
146 if (foundFirst) {
147 sum += 2;
148 } else if (found) {
149 sum += 1;
150 } else
151 return 0;
152 }
153 return sum;
154 }
155
156 int isMatchingGroup(String... words) {
157 return isMatching(groups, words);
158 }
159
160 int isMatchingName(String... words) {
161 return isMatching(names, words);
162 }
163
164 int isMatchingTags(String... words) {
165 return isMatching(tags, words);
166 }
167
168 @Override
169 public int compareTo(PresetClassification o) {
170 int result = o.classification - classification;
171 if (result == 0)
172 return preset.getName().compareTo(o.preset.getName());
173 else
174 return result;
175 }
176
177 @Override
178 public String toString() {
179 return Integer.toString(classification) + ' ' + preset;
180 }
181 }
182
183 /**
184 * Constructs a new {@code TaggingPresetSelector}.
185 * @param displayOnlyApplicable if {@code true} display "Show only applicable to selection" checkbox
186 * @param displaySearchInTags if {@code true} display "Search in tags" checkbox
187 */
188 public TaggingPresetSelector(boolean displayOnlyApplicable, boolean displaySearchInTags) {
189 super();
190 lsResult.setCellRenderer(new ResultListCellRenderer());
191 classifications.loadPresets(TaggingPresets.getTaggingPresets());
192
193 JPanel pnChecks = new JPanel();
194 pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
195
196 if (displayOnlyApplicable) {
197 ckOnlyApplicable = new JCheckBox();
198 ckOnlyApplicable.setText(tr("Show only applicable to selection"));
199 pnChecks.add(ckOnlyApplicable);
200 ckOnlyApplicable.addItemListener(e -> filterItems());
201 } else {
202 ckOnlyApplicable = null;
203 }
204
205 if (displaySearchInTags) {
206 ckSearchInTags = new JCheckBox();
207 ckSearchInTags.setText(tr("Search in tags"));
208 ckSearchInTags.setSelected(SEARCH_IN_TAGS.get());
209 ckSearchInTags.addItemListener(e -> filterItems());
210 pnChecks.add(ckSearchInTags);
211 } else {
212 ckSearchInTags = null;
213 }
214
215 add(pnChecks, BorderLayout.SOUTH);
216
217 setPreferredSize(new Dimension(400, 300));
218 filterItems();
219 JPopupMenu popupMenu = new JPopupMenu();
220 popupMenu.add(new AbstractAction(tr("Add toolbar button")) {
221 @Override
222 public void actionPerformed(ActionEvent ae) {
223 final TaggingPreset preset = getSelectedPreset();
224 if (preset != null) {
225 MainApplication.getToolbar().addCustomButton(preset.getToolbarString(), -1, false);
226 }
227 }
228 });
229 lsResult.addMouseListener(new PopupMenuLauncher(popupMenu));
230 }
231
232 /**
233 * Search expression can be in form: "group1/group2/name" where names can contain multiple words
234 */
235 @Override
236 protected synchronized void filterItems() {
237 //TODO Save favorites to file
238 String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
239 boolean onlyApplicable = ckOnlyApplicable != null && ckOnlyApplicable.isSelected();
240 boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected();
241
242 DataSet ds = Main.main.getEditDataSet();
243 Collection<OsmPrimitive> selected = (ds == null) ? Collections.<OsmPrimitive>emptyList() : ds.getSelected();
244 final List<PresetClassification> result = classifications.getMatchingPresets(
245 text, onlyApplicable, inTags, getTypesInSelection(), selected);
246
247 final TaggingPreset oldPreset = getSelectedPreset();
248 lsResultModel.setItems(Utils.transform(result, x -> x.preset));
249 final TaggingPreset newPreset = getSelectedPreset();
250 if (!Objects.equals(oldPreset, newPreset)) {
251 int[] indices = lsResult.getSelectedIndices();
252 for (ListSelectionListener listener : listSelectionListeners) {
253 listener.valueChanged(new ListSelectionEvent(lsResult, lsResult.getSelectedIndex(),
254 indices.length > 0 ? indices[indices.length-1] : -1, false));
255 }
256 }
257 }
258
259 /**
260 * A collection of {@link PresetClassification}s with the functionality of filtering wrt. searchString.
261 */
262 public static class PresetClassifications implements Iterable<PresetClassification> {
263
264 private final List<PresetClassification> classifications = new ArrayList<>();
265
266 public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags,
267 Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
268 final String[] groupWords;
269 final String[] nameWords;
270
271 if (searchText.contains("/")) {
272 groupWords = searchText.substring(0, searchText.lastIndexOf('/')).split("[\\s/]");
273 nameWords = searchText.substring(searchText.indexOf('/') + 1).split("\\s");
274 } else {
275 groupWords = null;
276 nameWords = searchText.split("\\s");
277 }
278
279 return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
280 }
281
282 public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable,
283 boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
284
285 final List<PresetClassification> result = new ArrayList<>();
286 for (PresetClassification presetClassification : classifications) {
287 TaggingPreset preset = presetClassification.preset;
288 presetClassification.classification = 0;
289
290 if (onlyApplicable) {
291 boolean suitable = preset.typeMatches(presetTypes);
292
293 if (!suitable && preset.types.contains(TaggingPresetType.RELATION)
294 && preset.roles != null && !preset.roles.roles.isEmpty()) {
295 suitable = preset.roles.roles.stream().anyMatch(
296 object -> object.memberExpression != null && selectedPrimitives.stream().anyMatch(object.memberExpression));
297 // keep the preset to allow the creation of new relations
298 }
299 if (!suitable) {
300 continue;
301 }
302 }
303
304 if (groupWords != null && presetClassification.isMatchingGroup(groupWords) == 0) {
305 continue;
306 }
307
308 int matchName = presetClassification.isMatchingName(nameWords);
309
310 if (matchName == 0) {
311 if (groupWords == null) {
312 int groupMatch = presetClassification.isMatchingGroup(nameWords);
313 if (groupMatch > 0) {
314 presetClassification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch;
315 }
316 }
317 if (presetClassification.classification == 0 && inTags) {
318 int tagsMatch = presetClassification.isMatchingTags(nameWords);
319 if (tagsMatch > 0) {
320 presetClassification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch;
321 }
322 }
323 } else {
324 presetClassification.classification = CLASSIFICATION_NAME_MATCH + matchName;
325 }
326
327 if (presetClassification.classification > 0) {
328 presetClassification.classification += presetClassification.favoriteIndex;
329 result.add(presetClassification);
330 }
331 }
332
333 Collections.sort(result);
334 return result;
335
336 }
337
338 public void clear() {
339 classifications.clear();
340 }
341
342 public void loadPresets(Collection<TaggingPreset> presets) {
343 for (TaggingPreset preset : presets) {
344 if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
345 continue;
346 }
347 classifications.add(new PresetClassification(preset));
348 }
349 }
350
351 @Override
352 public Iterator<PresetClassification> iterator() {
353 return classifications.iterator();
354 }
355 }
356
357 private Set<TaggingPresetType> getTypesInSelection() {
358 if (typesInSelectionDirty) {
359 synchronized (typesInSelection) {
360 typesInSelectionDirty = false;
361 typesInSelection.clear();
362 if (Main.main == null || Main.main.getEditDataSet() == null) return typesInSelection;
363 for (OsmPrimitive primitive : Main.main.getEditDataSet().getSelected()) {
364 typesInSelection.add(TaggingPresetType.forPrimitive(primitive));
365 }
366 }
367 }
368 return typesInSelection;
369 }
370
371 @Override
372 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
373 typesInSelectionDirty = true;
374 }
375
376 @Override
377 public synchronized void init() {
378 if (ckOnlyApplicable != null) {
379 ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
380 ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
381 }
382 super.init();
383 }
384
385 public void init(Collection<TaggingPreset> presets) {
386 classifications.clear();
387 classifications.loadPresets(presets);
388 init();
389 }
390
391 /**
392 * Save checkbox values in preferences for future reuse
393 */
394 public void savePreferences() {
395 if (ckSearchInTags != null) {
396 SEARCH_IN_TAGS.put(ckSearchInTags.isSelected());
397 }
398 if (ckOnlyApplicable != null && ckOnlyApplicable.isEnabled()) {
399 ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected());
400 }
401 }
402
403 /**
404 * Determines, which preset is selected at the moment.
405 * @return selected preset (as action)
406 */
407 public synchronized TaggingPreset getSelectedPreset() {
408 if (lsResultModel.isEmpty()) return null;
409 int idx = lsResult.getSelectedIndex();
410 if (idx < 0 || idx >= lsResultModel.getSize()) {
411 idx = 0;
412 }
413 return lsResultModel.getElementAt(idx);
414 }
415
416 /**
417 * Determines, which preset is selected at the moment. Updates {@link PresetClassification#favoriteIndex}!
418 * @return selected preset (as action)
419 */
420 public synchronized TaggingPreset getSelectedPresetAndUpdateClassification() {
421 final TaggingPreset preset = getSelectedPreset();
422 for (PresetClassification pc: classifications) {
423 if (pc.preset == preset) {
424 pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES;
425 } else if (pc.favoriteIndex > 0) {
426 pc.favoriteIndex--;
427 }
428 }
429 return preset;
430 }
431
432 public synchronized void setSelectedPreset(TaggingPreset p) {
433 lsResult.setSelectedValue(p, true);
434 }
435}
Note: See TracBrowser for help on using the repository browser.