001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging;
003
004import java.awt.BorderLayout;
005import java.awt.Component;
006import java.awt.Dimension;
007import java.awt.event.ActionListener;
008import java.awt.event.ItemEvent;
009import java.awt.event.ItemListener;
010import java.awt.event.KeyAdapter;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseAdapter;
013import java.awt.event.MouseEvent;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.Collections;
017import java.util.EnumSet;
018import java.util.HashSet;
019import java.util.List;
020import javax.swing.AbstractListModel;
021import javax.swing.Action;
022import javax.swing.BoxLayout;
023import javax.swing.DefaultListCellRenderer;
024import javax.swing.Icon;
025import javax.swing.JCheckBox;
026import javax.swing.JLabel;
027import javax.swing.JList;
028import javax.swing.JPanel;
029import javax.swing.JScrollPane;
030import javax.swing.event.DocumentEvent;
031import javax.swing.event.DocumentListener;
032import javax.swing.event.ListSelectionEvent;
033import javax.swing.event.ListSelectionListener;
034import org.openstreetmap.josm.Main;
035import org.openstreetmap.josm.data.SelectionChangedListener;
036import org.openstreetmap.josm.data.osm.Node;
037import org.openstreetmap.josm.data.osm.OsmPrimitive;
038import org.openstreetmap.josm.data.osm.Relation;
039import org.openstreetmap.josm.data.osm.Way;
040import org.openstreetmap.josm.data.preferences.BooleanProperty;
041import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
042import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key;
043import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.KeyedItem;
044import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role;
045import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles;
046import org.openstreetmap.josm.gui.widgets.JosmTextField;
047import static org.openstreetmap.josm.tools.I18n.tr;
048
049/**
050 * GUI component to select tagging preset: the list with filter and two checkboxes
051 * @since 6068
052 */
053public class TaggingPresetSelector extends JPanel implements SelectionChangedListener {
054    
055    private static final int CLASSIFICATION_IN_FAVORITES = 300;
056    private static final int CLASSIFICATION_NAME_MATCH = 300;
057    private static final int CLASSIFICATION_GROUP_MATCH = 200;
058    private static final int CLASSIFICATION_TAGS_MATCH = 100;
059
060    private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
061    private static final BooleanProperty ONLY_APPLICABLE  = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
062
063    
064    private JosmTextField edSearchText;
065    private JList lsResult;
066    private JCheckBox ckOnlyApplicable;
067    private JCheckBox ckSearchInTags;
068    private final EnumSet<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class);
069    private boolean typesInSelectionDirty = true;
070    private final List<PresetClassification> classifications = new ArrayList<PresetClassification>();
071    private ResultListModel lsResultModel = new ResultListModel();
072    
073    private ActionListener dblClickListener;
074    private ActionListener clickListener;
075
076    private static class ResultListCellRenderer extends DefaultListCellRenderer {
077        @Override
078        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
079                boolean cellHasFocus) {
080            JLabel result = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
081            TaggingPreset tp = (TaggingPreset)value;
082            result.setText(tp.getName());
083            result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
084            return result;
085        }
086    }
087
088    private static class ResultListModel extends AbstractListModel {
089
090        private List<PresetClassification> presets = new ArrayList<PresetClassification>();
091
092        public void setPresets(List<PresetClassification> presets) {
093            this.presets = presets;
094            fireContentsChanged(this, 0, Integer.MAX_VALUE);
095        }
096
097        public List<PresetClassification> getPresets() {
098            return presets;
099        }
100
101        @Override
102        public Object getElementAt(int index) {
103            return presets.get(index).preset;
104        }
105
106        @Override
107        public int getSize() {
108            return presets.size();
109        }
110
111    }
112
113    private static class PresetClassification implements Comparable<PresetClassification> {
114        public final TaggingPreset preset;
115        public int classification;
116        public int favoriteIndex;
117        private final Collection<String> groups = new HashSet<String>();
118        private final Collection<String> names = new HashSet<String>();
119        private final Collection<String> tags = new HashSet<String>();
120
121        PresetClassification(TaggingPreset preset) {
122            this.preset = preset;
123            TaggingPreset group = preset.group;
124            while (group != null) {
125                Collections.addAll(groups, group.getLocaleName().toLowerCase().split("\\s"));
126                group = group.group;
127            }
128            Collections.addAll(names, preset.getLocaleName().toLowerCase().split("\\s"));
129            for (TaggingPresetItem item: preset.data) {
130                if (item instanceof KeyedItem) {
131                    tags.add(((KeyedItem) item).key);
132                    if (item instanceof TaggingPresetItems.ComboMultiSelect) {
133                        final TaggingPresetItems.ComboMultiSelect cms = (TaggingPresetItems.ComboMultiSelect) item;
134                        if (Boolean.parseBoolean(cms.values_searchable)) {
135                            tags.addAll(cms.getDisplayValues());
136                        }
137                    }
138                    if (item instanceof Key && ((Key) item).value != null) {
139                        tags.add(((Key) item).value);
140                    }
141                } else if (item instanceof Roles) {
142                    for (Role role : ((Roles) item).roles) {
143                        tags.add(role.key);
144                    }
145                }
146            }
147        }
148
149        private int isMatching(Collection<String> values, String[] searchString) {
150            int sum = 0;
151            for (String word: searchString) {
152                boolean found = false;
153                boolean foundFirst = false;
154                for (String value: values) {
155                    int index = value.toLowerCase().indexOf(word);
156                    if (index == 0) {
157                        foundFirst = true;
158                        break;
159                    } else if (index > 0) {
160                        found = true;
161                    }
162                }
163                if (foundFirst) {
164                    sum += 2;
165                } else if (found) {
166                    sum += 1;
167                } else
168                    return 0;
169            }
170            return sum;
171        }
172
173        int isMatchingGroup(String[] words) {
174            return isMatching(groups, words);
175        }
176
177        int isMatchingName(String[] words) {
178            return isMatching(names, words);
179        }
180
181        int isMatchingTags(String[] words) {
182            return isMatching(tags, words);
183        }
184
185        @Override
186        public int compareTo(PresetClassification o) {
187            int result = o.classification - classification;
188            if (result == 0)
189                return preset.getName().compareTo(o.preset.getName());
190            else
191                return result;
192        }
193
194        @Override
195        public String toString() {
196            return classification + " " + preset.toString();
197        }
198    }
199
200    /**
201     * Constructs a new {@code TaggingPresetSelector}.
202     */
203    public TaggingPresetSelector() {
204        super(new BorderLayout());
205        if (TaggingPresetPreference.taggingPresets!=null) {
206            loadPresets(TaggingPresetPreference.taggingPresets);
207        }
208        
209        edSearchText = new JosmTextField();
210        edSearchText.getDocument().addDocumentListener(new DocumentListener() {
211            @Override public void removeUpdate(DocumentEvent e) { filterPresets(); }
212            @Override public void insertUpdate(DocumentEvent e) { filterPresets(); }
213            @Override public void changedUpdate(DocumentEvent e) { filterPresets(); }
214        });
215        edSearchText.addKeyListener(new KeyAdapter() {
216            @Override
217            public void keyPressed(KeyEvent e) {
218                switch (e.getKeyCode()) {
219                case KeyEvent.VK_DOWN:
220                    selectPreset(lsResult.getSelectedIndex() + 1);
221                    break;
222                case KeyEvent.VK_UP:
223                    selectPreset(lsResult.getSelectedIndex() - 1);
224                    break;
225                case KeyEvent.VK_PAGE_DOWN:
226                    selectPreset(lsResult.getSelectedIndex() + 10);
227                    break;
228                case KeyEvent.VK_PAGE_UP:
229                    selectPreset(lsResult.getSelectedIndex() - 10);
230                    break;
231                case KeyEvent.VK_HOME:
232                    selectPreset(0);
233                    break;
234                case KeyEvent.VK_END:
235                    selectPreset(lsResultModel.getSize());
236                    break;
237                }
238            }
239        });
240        add(edSearchText, BorderLayout.NORTH);
241
242        lsResult = new JList();
243        lsResult.setModel(lsResultModel);
244        lsResult.setCellRenderer(new ResultListCellRenderer());
245        lsResult.addMouseListener(new MouseAdapter() {
246            @Override
247            public void mouseClicked(MouseEvent e) {
248                if (e.getClickCount()>1) {
249                    if (dblClickListener!=null)
250                        dblClickListener.actionPerformed(null);
251                } else {
252                    if (clickListener!=null)
253                        clickListener.actionPerformed(null);
254                }
255            }
256        });
257        add(new JScrollPane(lsResult), BorderLayout.CENTER);
258
259        JPanel pnChecks = new JPanel();
260        pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
261
262        ckOnlyApplicable = new JCheckBox();
263        ckOnlyApplicable.setText(tr("Show only applicable to selection"));
264        pnChecks.add(ckOnlyApplicable);
265        ckOnlyApplicable.addItemListener(new ItemListener() {
266            @Override
267            public void itemStateChanged(ItemEvent e) {
268                filterPresets();
269            }
270        });
271
272        ckSearchInTags = new JCheckBox();
273        ckSearchInTags.setText(tr("Search in tags"));
274        ckSearchInTags.setSelected(SEARCH_IN_TAGS.get());
275        ckSearchInTags.addItemListener(new ItemListener() {
276            @Override
277            public void itemStateChanged(ItemEvent e) {
278                filterPresets();
279            }
280        });
281        pnChecks.add(ckSearchInTags);
282
283        add(pnChecks, BorderLayout.SOUTH);
284
285        setPreferredSize(new Dimension(400, 300));
286        
287        filterPresets();
288    }
289    
290    private void selectPreset(int newIndex) {
291        if (newIndex < 0) {
292            newIndex = 0;
293        }
294        if (newIndex > lsResultModel.getSize() - 1) {
295            newIndex = lsResultModel.getSize() - 1;
296        }
297        lsResult.setSelectedIndex(newIndex);
298        lsResult.ensureIndexIsVisible(newIndex);
299    }
300
301    /**
302     * Search expression can be in form: "group1/group2/name" where names can contain multiple words
303     */
304    private void filterPresets() {
305        //TODO Save favorites to file
306        String text = edSearchText.getText().toLowerCase();
307
308        String[] groupWords;
309        String[] nameWords;
310
311        if (text.contains("/")) {
312            groupWords = text.substring(0, text.lastIndexOf('/')).split("[\\s/]");
313            nameWords = text.substring(text.indexOf('/') + 1).split("\\s");
314        } else {
315            groupWords = null;
316            nameWords = text.split("\\s");
317        }
318
319        boolean onlyApplicable = ckOnlyApplicable.isSelected();
320        boolean inTags = ckSearchInTags.isSelected();
321
322        List<PresetClassification> result = new ArrayList<PresetClassification>();
323        PRESET_LOOP:
324            for (PresetClassification presetClasification: classifications) {
325                TaggingPreset preset = presetClasification.preset;
326                presetClasification.classification = 0;
327
328                if (onlyApplicable && preset.types != null) {
329                    boolean found = false;
330                    for (TaggingPresetType type: preset.types) {
331                        if (getTypesInSelection().contains(type)) {
332                            found = true;
333                            break;
334                        }
335                    }
336                    if (!found) {
337                        continue;
338                    }
339                }
340
341                if (groupWords != null && presetClasification.isMatchingGroup(groupWords) == 0) {
342                    continue PRESET_LOOP;
343                }
344
345                int matchName = presetClasification.isMatchingName(nameWords);
346
347                if (matchName == 0) {
348                    if (groupWords == null) {
349                        int groupMatch = presetClasification.isMatchingGroup(nameWords);
350                        if (groupMatch > 0) {
351                            presetClasification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch;
352                        }
353                    }
354                    if (presetClasification.classification == 0 && inTags) {
355                        int tagsMatch = presetClasification.isMatchingTags(nameWords);
356                        if (tagsMatch > 0) {
357                            presetClasification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch;
358                        }
359                    }
360                } else {
361                    presetClasification.classification = CLASSIFICATION_NAME_MATCH + matchName;
362                }
363
364                if (presetClasification.classification > 0) {
365                    presetClasification.classification += presetClasification.favoriteIndex;
366                    result.add(presetClasification);
367                }
368            }
369
370        Collections.sort(result);
371        lsResultModel.setPresets(result);
372
373    }
374    
375    private EnumSet<TaggingPresetType> getTypesInSelection() {
376        if (typesInSelectionDirty) {
377            synchronized (typesInSelection) {
378                typesInSelectionDirty = false;
379                typesInSelection.clear();
380                if (Main.main==null || Main.main.getCurrentDataSet() == null) return typesInSelection;
381                for (OsmPrimitive primitive : Main.main.getCurrentDataSet().getSelected()) {
382                    if (primitive instanceof Node) {
383                        typesInSelection.add(TaggingPresetType.NODE);
384                    } else if (primitive instanceof Way) {
385                        typesInSelection.add(TaggingPresetType.WAY);
386                        if (((Way) primitive).isClosed()) {
387                            typesInSelection.add(TaggingPresetType.CLOSEDWAY);
388                        }
389                    } else if (primitive instanceof Relation) {
390                        typesInSelection.add(TaggingPresetType.RELATION);
391                    }
392                }
393            }
394        }
395        return typesInSelection;
396    }
397    
398    @Override
399    public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
400        typesInSelectionDirty = true;
401    }
402
403    public void init() {
404        ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
405        ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
406        edSearchText.setText("");
407        filterPresets();
408    }
409    
410    public void init(Collection<TaggingPreset> presets) {
411        classifications.clear();
412        loadPresets(presets);
413        init();
414    }
415
416    
417    public void clearSelection() {
418        lsResult.getSelectionModel().clearSelection();
419    }
420    
421    /**
422     * Save checkbox values in preferences for future reuse
423     */
424    public void savePreferences() {
425        SEARCH_IN_TAGS.put(ckSearchInTags.isSelected());
426        if (ckOnlyApplicable.isEnabled()) {
427            ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected());
428        }
429    }
430    
431    /**
432     * Determines, which preset is selected at the current moment
433     * @return selected preset (as action)
434     */
435    public TaggingPreset getSelectedPreset() {
436        List<PresetClassification> presets = lsResultModel.getPresets();
437        if (presets.isEmpty()) return null;
438        int idx = lsResult.getSelectedIndex();
439        if (idx == -1) {
440            idx = 0;
441        }
442        TaggingPreset preset = presets.get(idx).preset;
443        for (PresetClassification pc: classifications) {
444            if (pc.preset == preset) {
445                pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES;
446            } else if (pc.favoriteIndex > 0) {
447                pc.favoriteIndex--;
448            }
449        }
450        return preset;
451    }
452
453    private void loadPresets(Collection<TaggingPreset> presets) {
454        for (TaggingPreset preset: presets) {
455            if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
456                continue;
457            }
458            classifications.add(new PresetClassification(preset));
459        }
460    }
461
462    public void setSelectedPreset(TaggingPreset p) {
463        lsResult.setSelectedValue(p, true);
464    }
465    
466    public int getItemCount() {
467        return lsResultModel.getSize();
468    }
469    
470    public void setDblClickListener(ActionListener dblClickListener) {
471        this.dblClickListener = dblClickListener;
472    }
473    
474    public void setClickListener(ActionListener clickListener) {
475        this.clickListener = clickListener;
476    }
477    
478    public void addSelectionListener(final ActionListener selectListener) {
479        lsResult.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
480            @Override
481            public void valueChanged(ListSelectionEvent e) {
482                if (!e.getValueIsAdjusting())
483                    selectListener.actionPerformed(null);
484            }
485        });
486    }
487}