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 }