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

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

see #15229 - deprecate Main.main - new class OsmDataManager

  • 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.data.osm.DataSelectionListener;
37import org.openstreetmap.josm.data.osm.DataSet;
38import org.openstreetmap.josm.data.osm.OsmDataManager;
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 DataSelectionListener {
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 = OsmDataManager.getInstance().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 (OsmDataManager.getInstance().getEditDataSet() == null) return typesInSelection;
363                for (OsmPrimitive primitive : OsmDataManager.getInstance().getEditDataSet().getSelected()) {
364                    typesInSelection.add(TaggingPresetType.forPrimitive(primitive));
365                }
366            }
367        }
368        return typesInSelection;
369    }
370
371    @Override
372    public void selectionChanged(SelectionChangeEvent event) {
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.