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

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

fix #14152 - NPE

  • Property svn:eol-style set to native
File size: 17.1 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 addLocaleNames(groups, group);
97 group = group.group;
98 }
99 addLocaleNames(names, preset);
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 void addLocaleNames(Collection<String> collection, TaggingPreset preset) {
121 String locName = preset.getLocaleName();
122 if (locName != null) {
123 Collections.addAll(collection, locName.toLowerCase(Locale.ENGLISH).split("\\s"));
124 }
125 }
126
127 private static int isMatching(Collection<String> values, String ... searchString) {
128 int sum = 0;
129 for (String word: searchString) {
130 boolean found = false;
131 boolean foundFirst = false;
132 for (String value: values) {
133 int index = value.toLowerCase(Locale.ENGLISH).indexOf(word);
134 if (index == 0) {
135 foundFirst = true;
136 break;
137 } else if (index > 0) {
138 found = true;
139 }
140 }
141 if (foundFirst) {
142 sum += 2;
143 } else if (found) {
144 sum += 1;
145 } else
146 return 0;
147 }
148 return sum;
149 }
150
151 int isMatchingGroup(String ... words) {
152 return isMatching(groups, words);
153 }
154
155 int isMatchingName(String ... words) {
156 return isMatching(names, words);
157 }
158
159 int isMatchingTags(String ... words) {
160 return isMatching(tags, words);
161 }
162
163 @Override
164 public int compareTo(PresetClassification o) {
165 int result = o.classification - classification;
166 if (result == 0)
167 return preset.getName().compareTo(o.preset.getName());
168 else
169 return result;
170 }
171
172 @Override
173 public String toString() {
174 return Integer.toString(classification) + ' ' + preset;
175 }
176 }
177
178 /**
179 * Constructs a new {@code TaggingPresetSelector}.
180 * @param displayOnlyApplicable if {@code true} display "Show only applicable to selection" checkbox
181 * @param displaySearchInTags if {@code true} display "Search in tags" checkbox
182 */
183 public TaggingPresetSelector(boolean displayOnlyApplicable, boolean displaySearchInTags) {
184 super();
185 lsResult.setCellRenderer(new ResultListCellRenderer());
186 classifications.loadPresets(TaggingPresets.getTaggingPresets());
187
188 JPanel pnChecks = new JPanel();
189 pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
190
191 if (displayOnlyApplicable) {
192 ckOnlyApplicable = new JCheckBox();
193 ckOnlyApplicable.setText(tr("Show only applicable to selection"));
194 pnChecks.add(ckOnlyApplicable);
195 ckOnlyApplicable.addItemListener(e -> filterItems());
196 } else {
197 ckOnlyApplicable = null;
198 }
199
200 if (displaySearchInTags) {
201 ckSearchInTags = new JCheckBox();
202 ckSearchInTags.setText(tr("Search in tags"));
203 ckSearchInTags.setSelected(SEARCH_IN_TAGS.get());
204 ckSearchInTags.addItemListener(e -> filterItems());
205 pnChecks.add(ckSearchInTags);
206 } else {
207 ckSearchInTags = null;
208 }
209
210 add(pnChecks, BorderLayout.SOUTH);
211
212 setPreferredSize(new Dimension(400, 300));
213 filterItems();
214 JPopupMenu popupMenu = new JPopupMenu();
215 popupMenu.add(new AbstractAction(tr("Add toolbar button")) {
216 @Override
217 public void actionPerformed(ActionEvent ae) {
218 final TaggingPreset preset = getSelectedPreset();
219 if (preset != null) {
220 Main.toolbar.addCustomButton(preset.getToolbarString(), -1, false);
221 }
222 }
223 });
224 lsResult.addMouseListener(new PopupMenuLauncher(popupMenu));
225 }
226
227 /**
228 * Search expression can be in form: "group1/group2/name" where names can contain multiple words
229 */
230 @Override
231 protected synchronized void filterItems() {
232 //TODO Save favorites to file
233 String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
234 boolean onlyApplicable = ckOnlyApplicable != null && ckOnlyApplicable.isSelected();
235 boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected();
236
237 DataSet ds = Main.getLayerManager().getEditDataSet();
238 Collection<OsmPrimitive> selected = (ds == null) ? Collections.<OsmPrimitive>emptyList() : ds.getSelected();
239 final List<PresetClassification> result = classifications.getMatchingPresets(
240 text, onlyApplicable, inTags, getTypesInSelection(), selected);
241
242 final TaggingPreset oldPreset = getSelectedPreset();
243 lsResultModel.setItems(Utils.transform(result, x -> x.preset));
244 final TaggingPreset newPreset = getSelectedPreset();
245 if (!Objects.equals(oldPreset, newPreset)) {
246 int[] indices = lsResult.getSelectedIndices();
247 for (ListSelectionListener listener : listSelectionListeners) {
248 listener.valueChanged(new ListSelectionEvent(lsResult, lsResult.getSelectedIndex(),
249 indices.length > 0 ? indices[indices.length-1] : -1, false));
250 }
251 }
252 }
253
254 /**
255 * A collection of {@link PresetClassification}s with the functionality of filtering wrt. searchString.
256 */
257 public static class PresetClassifications implements Iterable<PresetClassification> {
258
259 private final List<PresetClassification> classifications = new ArrayList<>();
260
261 public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags,
262 Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
263 final String[] groupWords;
264 final String[] nameWords;
265
266 if (searchText.contains("/")) {
267 groupWords = searchText.substring(0, searchText.lastIndexOf('/')).split("[\\s/]");
268 nameWords = searchText.substring(searchText.indexOf('/') + 1).split("\\s");
269 } else {
270 groupWords = null;
271 nameWords = searchText.split("\\s");
272 }
273
274 return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
275 }
276
277 public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable,
278 boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
279
280 final List<PresetClassification> result = new ArrayList<>();
281 for (PresetClassification presetClassification : classifications) {
282 TaggingPreset preset = presetClassification.preset;
283 presetClassification.classification = 0;
284
285 if (onlyApplicable) {
286 boolean suitable = preset.typeMatches(presetTypes);
287
288 if (!suitable && preset.types.contains(TaggingPresetType.RELATION)
289 && preset.roles != null && !preset.roles.roles.isEmpty()) {
290 suitable = preset.roles.roles.stream().anyMatch(
291 object -> object.memberExpression != null && selectedPrimitives.stream().anyMatch(object.memberExpression));
292 // keep the preset to allow the creation of new relations
293 }
294 if (!suitable) {
295 continue;
296 }
297 }
298
299 if (groupWords != null && presetClassification.isMatchingGroup(groupWords) == 0) {
300 continue;
301 }
302
303 int matchName = presetClassification.isMatchingName(nameWords);
304
305 if (matchName == 0) {
306 if (groupWords == null) {
307 int groupMatch = presetClassification.isMatchingGroup(nameWords);
308 if (groupMatch > 0) {
309 presetClassification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch;
310 }
311 }
312 if (presetClassification.classification == 0 && inTags) {
313 int tagsMatch = presetClassification.isMatchingTags(nameWords);
314 if (tagsMatch > 0) {
315 presetClassification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch;
316 }
317 }
318 } else {
319 presetClassification.classification = CLASSIFICATION_NAME_MATCH + matchName;
320 }
321
322 if (presetClassification.classification > 0) {
323 presetClassification.classification += presetClassification.favoriteIndex;
324 result.add(presetClassification);
325 }
326 }
327
328 Collections.sort(result);
329 return result;
330
331 }
332
333 public void clear() {
334 classifications.clear();
335 }
336
337 public void loadPresets(Collection<TaggingPreset> presets) {
338 for (TaggingPreset preset : presets) {
339 if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
340 continue;
341 }
342 classifications.add(new PresetClassification(preset));
343 }
344 }
345
346 @Override
347 public Iterator<PresetClassification> iterator() {
348 return classifications.iterator();
349 }
350 }
351
352 private Set<TaggingPresetType> getTypesInSelection() {
353 if (typesInSelectionDirty) {
354 synchronized (typesInSelection) {
355 typesInSelectionDirty = false;
356 typesInSelection.clear();
357 if (Main.main == null || Main.getLayerManager().getEditDataSet() == null) return typesInSelection;
358 for (OsmPrimitive primitive : Main.getLayerManager().getEditDataSet().getSelected()) {
359 typesInSelection.add(TaggingPresetType.forPrimitive(primitive));
360 }
361 }
362 }
363 return typesInSelection;
364 }
365
366 @Override
367 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
368 typesInSelectionDirty = true;
369 }
370
371 @Override
372 public synchronized void init() {
373 if (ckOnlyApplicable != null) {
374 ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
375 ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
376 }
377 super.init();
378 }
379
380 public void init(Collection<TaggingPreset> presets) {
381 classifications.clear();
382 classifications.loadPresets(presets);
383 init();
384 }
385
386 /**
387 * Save checkbox values in preferences for future reuse
388 */
389 public void savePreferences() {
390 if (ckSearchInTags != null) {
391 SEARCH_IN_TAGS.put(ckSearchInTags.isSelected());
392 }
393 if (ckOnlyApplicable != null && ckOnlyApplicable.isEnabled()) {
394 ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected());
395 }
396 }
397
398 /**
399 * Determines, which preset is selected at the moment.
400 * @return selected preset (as action)
401 */
402 public synchronized TaggingPreset getSelectedPreset() {
403 if (lsResultModel.isEmpty()) return null;
404 int idx = lsResult.getSelectedIndex();
405 if (idx < 0 || idx >= lsResultModel.getSize()) {
406 idx = 0;
407 }
408 return lsResultModel.getElementAt(idx);
409 }
410
411 /**
412 * Determines, which preset is selected at the moment. Updates {@link PresetClassification#favoriteIndex}!
413 * @return selected preset (as action)
414 */
415 public synchronized TaggingPreset getSelectedPresetAndUpdateClassification() {
416 final TaggingPreset preset = getSelectedPreset();
417 for (PresetClassification pc: classifications) {
418 if (pc.preset == preset) {
419 pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES;
420 } else if (pc.favoriteIndex > 0) {
421 pc.favoriteIndex--;
422 }
423 }
424 return preset;
425 }
426
427 public synchronized void setSelectedPreset(TaggingPreset p) {
428 lsResult.setSelectedValue(p, true);
429 }
430}
Note: See TracBrowser for help on using the repository browser.