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