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

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

sonar - pmd:UseVarargs - Use Varargs

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