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

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

sonar - Performance - Method passes constant String of length 1 to character overridden method + add unit tests/javadoc

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