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 }