001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.search; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trc; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.Cursor; 010import java.awt.Dimension; 011import java.awt.FlowLayout; 012import java.awt.GridBagLayout; 013import java.awt.event.ActionEvent; 014import java.awt.event.KeyEvent; 015import java.awt.event.MouseAdapter; 016import java.awt.event.MouseEvent; 017import java.util.ArrayList; 018import java.util.Arrays; 019import java.util.Collection; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.LinkedHashSet; 023import java.util.LinkedList; 024import java.util.List; 025import java.util.Map; 026import java.util.Objects; 027import java.util.Set; 028 029import javax.swing.ButtonGroup; 030import javax.swing.JCheckBox; 031import javax.swing.JLabel; 032import javax.swing.JOptionPane; 033import javax.swing.JPanel; 034import javax.swing.JRadioButton; 035import javax.swing.text.BadLocationException; 036import javax.swing.text.JTextComponent; 037 038import org.openstreetmap.josm.Main; 039import org.openstreetmap.josm.actions.ActionParameter; 040import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter; 041import org.openstreetmap.josm.actions.JosmAction; 042import org.openstreetmap.josm.actions.ParameterizedAction; 043import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; 044import org.openstreetmap.josm.data.osm.DataSet; 045import org.openstreetmap.josm.data.osm.Filter; 046import org.openstreetmap.josm.data.osm.OsmPrimitive; 047import org.openstreetmap.josm.gui.ExtendedDialog; 048import org.openstreetmap.josm.gui.PleaseWaitRunnable; 049import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException; 050import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 051import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser; 052import org.openstreetmap.josm.gui.progress.ProgressMonitor; 053import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 054import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 055import org.openstreetmap.josm.tools.GBC; 056import org.openstreetmap.josm.tools.Predicate; 057import org.openstreetmap.josm.tools.Shortcut; 058import org.openstreetmap.josm.tools.Utils; 059 060public class SearchAction extends JosmAction implements ParameterizedAction { 061 062 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15; 063 /** Maximum number of characters before the search expression is shortened for display purposes. */ 064 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100; 065 066 private static final String SEARCH_EXPRESSION = "searchExpression"; 067 068 public enum SearchMode { 069 /** replace selection */ 070 replace('R'), 071 /** add to selection */ 072 add('A'), 073 /** remove from selection */ 074 remove('D'), 075 /** find in selection */ 076 in_selection('S'); 077 078 private final char code; 079 080 SearchMode(char code) { 081 this.code = code; 082 } 083 084 /** 085 * Returns the unique character code of this mode. 086 * @return the unique character code of this mode 087 */ 088 public char getCode() { 089 return code; 090 } 091 092 /** 093 * Returns the search mode matching the given character code. 094 * @param code character code 095 * @return search mode matching the given character code 096 */ 097 public static SearchMode fromCode(char code) { 098 for (SearchMode mode: values()) { 099 if (mode.getCode() == code) 100 return mode; 101 } 102 return null; 103 } 104 } 105 106 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>(); 107 static { 108 for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) { 109 SearchSetting ss = SearchSetting.readFromString(s); 110 if (ss != null) { 111 searchHistory.add(ss); 112 } 113 } 114 } 115 116 public static Collection<SearchSetting> getSearchHistory() { 117 return searchHistory; 118 } 119 120 public static void saveToHistory(SearchSetting s) { 121 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) { 122 searchHistory.addFirst(new SearchSetting(s)); 123 } else if (searchHistory.contains(s)) { 124 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries 125 searchHistory.remove(s); 126 searchHistory.addFirst(new SearchSetting(s)); 127 } 128 int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE); 129 while (searchHistory.size() > maxsize) { 130 searchHistory.removeLast(); 131 } 132 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size()); 133 for (SearchSetting item: searchHistory) { 134 savedHistory.add(item.writeToString()); 135 } 136 Main.pref.putCollection("search.history", savedHistory); 137 } 138 139 public static List<String> getSearchExpressionHistory() { 140 List<String> ret = new ArrayList<>(getSearchHistory().size()); 141 for (SearchSetting ss: getSearchHistory()) { 142 ret.add(ss.text); 143 } 144 return ret; 145 } 146 147 private static volatile SearchSetting lastSearch; 148 149 /** 150 * Constructs a new {@code SearchAction}. 151 */ 152 public SearchAction() { 153 super(tr("Search..."), "dialogs/search", tr("Search for objects."), 154 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true); 155 putValue("help", ht("/Action/Search")); 156 } 157 158 @Override 159 public void actionPerformed(ActionEvent e) { 160 if (!isEnabled()) 161 return; 162 search(); 163 } 164 165 @Override 166 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) { 167 if (parameters.get(SEARCH_EXPRESSION) == null) { 168 actionPerformed(e); 169 } else { 170 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION)); 171 } 172 } 173 174 private static class DescriptionTextBuilder { 175 176 private final StringBuilder s = new StringBuilder(4096); 177 178 public StringBuilder append(String string) { 179 return s.append(string); 180 } 181 182 StringBuilder appendItem(String item) { 183 return append("<li>").append(item).append("</li>\n"); 184 } 185 186 StringBuilder appendItemHeader(String itemHeader) { 187 return append("<li class=\"header\">").append(itemHeader).append("</li>\n"); 188 } 189 190 @Override 191 public String toString() { 192 return s.toString(); 193 } 194 } 195 196 private static class SearchKeywordRow extends JPanel { 197 198 private final HistoryComboBox hcb; 199 200 SearchKeywordRow(HistoryComboBox hcb) { 201 super(new FlowLayout(FlowLayout.LEFT)); 202 this.hcb = hcb; 203 } 204 205 public SearchKeywordRow addTitle(String title) { 206 add(new JLabel(tr("{0}: ", title))); 207 return this; 208 } 209 210 public SearchKeywordRow addKeyword(String displayText, final String insertText, String description, String... examples) { 211 JLabel label = new JLabel("<html>" 212 + "<style>td{border:1px solid gray; font-weight:normal;}</style>" 213 + "<table><tr><td>" + displayText + "</td></tr></table></html>"); 214 add(label); 215 if (description != null || examples.length > 0) { 216 label.setToolTipText("<html>" 217 + description 218 + (examples.length > 0 ? Utils.joinAsHtmlUnorderedList(Arrays.asList(examples)) : "") 219 + "</html>"); 220 } 221 if (insertText != null) { 222 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 223 label.addMouseListener(new MouseAdapter() { 224 225 @Override 226 public void mouseClicked(MouseEvent e) { 227 try { 228 JTextComponent tf = hcb.getEditorComponent(); 229 tf.getDocument().insertString(tf.getCaretPosition(), ' ' + insertText, null); 230 } catch (BadLocationException ex) { 231 throw new RuntimeException(ex.getMessage(), ex); 232 } 233 } 234 }); 235 } 236 return this; 237 } 238 } 239 240 public static SearchSetting showSearchDialog(SearchSetting initialValues) { 241 if (initialValues == null) { 242 initialValues = new SearchSetting(); 243 } 244 // -- prepare the combo box with the search expressions 245 // 246 JLabel label = new JLabel(initialValues instanceof Filter ? tr("Filter string:") : tr("Search string:")); 247 final HistoryComboBox hcbSearchString = new HistoryComboBox(); 248 final String tooltip = tr("Enter the search expression"); 249 hcbSearchString.setText(initialValues.text); 250 hcbSearchString.setToolTipText(tooltip); 251 // we have to reverse the history, because ComboBoxHistory will reverse it again in addElement() 252 // 253 List<String> searchExpressionHistory = getSearchExpressionHistory(); 254 Collections.reverse(searchExpressionHistory); 255 hcbSearchString.setPossibleItems(searchExpressionHistory); 256 hcbSearchString.setPreferredSize(new Dimension(40, hcbSearchString.getPreferredSize().height)); 257 label.setLabelFor(hcbSearchString); 258 259 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace); 260 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add); 261 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove); 262 JRadioButton inSelection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection); 263 ButtonGroup bg = new ButtonGroup(); 264 bg.add(replace); 265 bg.add(add); 266 bg.add(remove); 267 bg.add(inSelection); 268 269 final JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive); 270 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements); 271 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search.")); 272 final JRadioButton standardSearch = new JRadioButton(tr("standard"), !initialValues.regexSearch && !initialValues.mapCSSSearch); 273 final JRadioButton regexSearch = new JRadioButton(tr("regular expression"), initialValues.regexSearch); 274 final JRadioButton mapCSSSearch = new JRadioButton(tr("MapCSS selector"), initialValues.mapCSSSearch); 275 final JCheckBox addOnToolbar = new JCheckBox(tr("add toolbar button"), false); 276 final ButtonGroup bg2 = new ButtonGroup(); 277 bg2.add(standardSearch); 278 bg2.add(regexSearch); 279 bg2.add(mapCSSSearch); 280 281 JPanel top = new JPanel(new GridBagLayout()); 282 top.add(label, GBC.std().insets(0, 0, 5, 0)); 283 top.add(hcbSearchString, GBC.eol().fill(GBC.HORIZONTAL)); 284 JPanel left = new JPanel(new GridBagLayout()); 285 left.add(replace, GBC.eol()); 286 left.add(add, GBC.eol()); 287 left.add(remove, GBC.eol()); 288 left.add(inSelection, GBC.eop()); 289 left.add(caseSensitive, GBC.eol()); 290 if (Main.pref.getBoolean("expert", false)) { 291 left.add(allElements, GBC.eol()); 292 left.add(addOnToolbar, GBC.eop()); 293 left.add(standardSearch, GBC.eol()); 294 left.add(regexSearch, GBC.eol()); 295 left.add(mapCSSSearch, GBC.eol()); 296 } 297 298 final JPanel right; 299 right = new JPanel(new GridBagLayout()); 300 buildHints(right, hcbSearchString); 301 302 final JTextComponent editorComponent = hcbSearchString.getEditorComponent(); 303 editorComponent.getDocument().addDocumentListener(new AbstractTextComponentValidator(editorComponent) { 304 305 @Override 306 public void validate() { 307 if (!isValid()) { 308 feedbackInvalid(tr("Invalid search expression")); 309 } else { 310 feedbackValid(tooltip); 311 } 312 } 313 314 @Override 315 public boolean isValid() { 316 try { 317 SearchSetting ss = new SearchSetting(); 318 ss.text = hcbSearchString.getText(); 319 ss.caseSensitive = caseSensitive.isSelected(); 320 ss.regexSearch = regexSearch.isSelected(); 321 ss.mapCSSSearch = mapCSSSearch.isSelected(); 322 SearchCompiler.compile(ss); 323 return true; 324 } catch (ParseError | MapCSSException e) { 325 return false; 326 } 327 } 328 }); 329 330 final JPanel p = new JPanel(new GridBagLayout()); 331 p.add(top, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 0)); 332 p.add(left, GBC.std().anchor(GBC.NORTH).insets(5, 10, 10, 0)); 333 p.add(right, GBC.eol()); 334 ExtendedDialog dialog = new ExtendedDialog( 335 Main.parent, 336 initialValues instanceof Filter ? tr("Filter") : tr("Search"), 337 new String[] { 338 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"), 339 tr("Cancel")} 340 ) { 341 @Override 342 protected void buttonAction(int buttonIndex, ActionEvent evt) { 343 if (buttonIndex == 0) { 344 try { 345 SearchSetting ss = new SearchSetting(); 346 ss.text = hcbSearchString.getText(); 347 ss.caseSensitive = caseSensitive.isSelected(); 348 ss.regexSearch = regexSearch.isSelected(); 349 ss.mapCSSSearch = mapCSSSearch.isSelected(); 350 SearchCompiler.compile(ss); 351 super.buttonAction(buttonIndex, evt); 352 } catch (ParseError e) { 353 Main.debug(e); 354 JOptionPane.showMessageDialog( 355 Main.parent, 356 tr("Search expression is not valid: \n\n {0}", e.getMessage()), 357 tr("Invalid search expression"), 358 JOptionPane.ERROR_MESSAGE); 359 } 360 } else { 361 super.buttonAction(buttonIndex, evt); 362 } 363 } 364 }; 365 dialog.setButtonIcons(new String[] {"dialogs/search", "cancel"}); 366 dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */); 367 dialog.setContent(p); 368 dialog.showDialog(); 369 int result = dialog.getValue(); 370 371 if (result != 1) return null; 372 373 // User pressed OK - let's perform the search 374 SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace 375 : (add.isSelected() ? SearchAction.SearchMode.add 376 : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection)); 377 initialValues.text = hcbSearchString.getText(); 378 initialValues.mode = mode; 379 initialValues.caseSensitive = caseSensitive.isSelected(); 380 initialValues.allElements = allElements.isSelected(); 381 initialValues.regexSearch = regexSearch.isSelected(); 382 initialValues.mapCSSSearch = mapCSSSearch.isSelected(); 383 384 if (addOnToolbar.isSelected()) { 385 ToolbarPreferences.ActionDefinition aDef = 386 new ToolbarPreferences.ActionDefinition(Main.main.menu.search); 387 aDef.getParameters().put(SEARCH_EXPRESSION, initialValues); 388 // Display search expression as tooltip instead of generic one 389 aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY)); 390 // parametrized action definition is now composed 391 ActionParser actionParser = new ToolbarPreferences.ActionParser(null); 392 String res = actionParser.saveAction(aDef); 393 394 // add custom search button to toolbar preferences 395 Main.toolbar.addCustomButton(res, -1, false); 396 } 397 return initialValues; 398 } 399 400 private static void buildHints(JPanel right, HistoryComboBox hcbSearchString) { 401 right.add(new SearchKeywordRow(hcbSearchString) 402 .addTitle(tr("basic examples")) 403 .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key")) 404 .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key")), 405 GBC.eol()); 406 right.add(new SearchKeywordRow(hcbSearchString) 407 .addTitle(tr("basics")) 408 .addKeyword("<i>key</i>:<i>valuefragment</i>", null, 409 tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet") 410 .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''")) 411 .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''")) 412 .addKeyword("<i>key</i>=*", null, tr("''key'' with any value")) 413 .addKeyword("*=<i>value</i>", null, tr("''value'' in any key")) 414 .addKeyword("<i>key</i>=", null, tr("matches if ''key'' exists")) 415 .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)")) 416 .addKeyword("\"key\"=\"value\"", "\"\"=\"\"", 417 tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " + 418 "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."), 419 "\"addr:street\""), 420 GBC.eol()); 421 right.add(new SearchKeywordRow(hcbSearchString) 422 .addTitle(tr("combinators")) 423 .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)")) 424 .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)")) 425 .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)")) 426 .addKeyword("-<i>expr</i>", null, tr("logical not")) 427 .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")), 428 GBC.eol()); 429 430 if (Main.pref.getBoolean("expert", false)) { 431 right.add(new SearchKeywordRow(hcbSearchString) 432 .addTitle(tr("objects")) 433 .addKeyword("type:node", "type:node ", tr("all ways")) 434 .addKeyword("type:way", "type:way ", tr("all ways")) 435 .addKeyword("type:relation", "type:relation ", tr("all relations")) 436 .addKeyword("closed", "closed ", tr("all closed ways")) 437 .addKeyword("untagged", "untagged ", tr("object without useful tags")), 438 GBC.eol()); 439 right.add(new SearchKeywordRow(hcbSearchString) 440 .addTitle(tr("metadata")) 441 .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous")) 442 .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)") 443 .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)") 444 .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"), 445 "changeset:0 (objects without an assigned changeset)") 446 .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/", 447 "timestamp:2008/2011-02-04T12"), 448 GBC.eol()); 449 right.add(new SearchKeywordRow(hcbSearchString) 450 .addTitle(tr("properties")) 451 .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes")) 452 .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways")) 453 .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags")) 454 .addKeyword("role:", "role:", tr("objects with given role in a relation")) 455 .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2")) 456 .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")), 457 GBC.eol()); 458 right.add(new SearchKeywordRow(hcbSearchString) 459 .addTitle(tr("state")) 460 .addKeyword("modified", "modified ", tr("all modified objects")) 461 .addKeyword("new", "new ", tr("all new objects")) 462 .addKeyword("selected", "selected ", tr("all selected objects")) 463 .addKeyword("incomplete", "incomplete ", tr("all incomplete objects")), 464 GBC.eol()); 465 right.add(new SearchKeywordRow(hcbSearchString) 466 .addTitle(tr("related objects")) 467 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building") 468 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop") 469 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>")) 470 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>")) 471 .addKeyword("nth:<i>7</i>", "nth:", 472 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1") 473 .addKeyword("nth%:<i>7</i>", "nth%:", 474 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"), 475 GBC.eol()); 476 right.add(new SearchKeywordRow(hcbSearchString) 477 .addTitle(tr("view")) 478 .addKeyword("inview", "inview ", tr("objects in current view")) 479 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view")) 480 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area")) 481 .addKeyword("allindownloadedarea", "allindownloadedarea ", 482 tr("objects (and all its way nodes / relation members) in downloaded area")), 483 GBC.eol()); 484 } 485 } 486 487 /** 488 * Launches the dialog for specifying search criteria and runs a search 489 */ 490 public static void search() { 491 SearchSetting se = showSearchDialog(lastSearch); 492 if (se != null) { 493 searchWithHistory(se); 494 } 495 } 496 497 /** 498 * Adds the search specified by the settings in <code>s</code> to the 499 * search history and performs the search. 500 * 501 * @param s search settings 502 */ 503 public static void searchWithHistory(SearchSetting s) { 504 saveToHistory(s); 505 lastSearch = new SearchSetting(s); 506 search(s); 507 } 508 509 /** 510 * Performs the search specified by the settings in <code>s</code> without saving it to search history. 511 * 512 * @param s search settings 513 */ 514 public static void searchWithoutHistory(SearchSetting s) { 515 lastSearch = new SearchSetting(s); 516 search(s); 517 } 518 519 /** 520 * Performs the search specified by the search string {@code search} and the search mode {@code mode}. 521 * 522 * @param search the search string to use 523 * @param mode the search mode to use 524 */ 525 public static void search(String search, SearchMode mode) { 526 final SearchSetting searchSetting = new SearchSetting(); 527 searchSetting.text = search; 528 searchSetting.mode = mode; 529 search(searchSetting); 530 } 531 532 static void search(SearchSetting s) { 533 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run(); 534 } 535 536 /** 537 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search. 538 * 539 * @param search the search string to use 540 * @param mode the search mode to use 541 * @return The result of the search. 542 * @since 10457 543 */ 544 public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) { 545 final SearchSetting searchSetting = new SearchSetting(); 546 searchSetting.text = search; 547 searchSetting.mode = mode; 548 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 549 SearchTask.newSearchTask(searchSetting, receiver).run(); 550 return receiver.result; 551 } 552 553 /** 554 * Interfaces implementing this may receive the result of the current search. 555 * @author Michael Zangl 556 * @since 10457 557 */ 558 interface SearchReceiver { 559 /** 560 * Receive the search result 561 * @param ds The data set searched on. 562 * @param result The result collection, including the initial collection. 563 * @param foundMatches The number of matches added to the result. 564 * @param setting The setting used. 565 */ 566 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting); 567 } 568 569 /** 570 * Select the search result and display a status text for it. 571 */ 572 private static class SelectSearchReceiver implements SearchReceiver { 573 574 @Override 575 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) { 576 ds.setSelected(result); 577 if (foundMatches == 0) { 578 final String msg; 579 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY); 580 if (setting.mode == SearchMode.replace) { 581 msg = tr("No match found for ''{0}''", text); 582 } else if (setting.mode == SearchMode.add) { 583 msg = tr("Nothing added to selection by searching for ''{0}''", text); 584 } else if (setting.mode == SearchMode.remove) { 585 msg = tr("Nothing removed from selection by searching for ''{0}''", text); 586 } else if (setting.mode == SearchMode.in_selection) { 587 msg = tr("Nothing found in selection by searching for ''{0}''", text); 588 } else { 589 msg = null; 590 } 591 Main.map.statusLine.setHelpText(msg); 592 JOptionPane.showMessageDialog( 593 Main.parent, 594 msg, 595 tr("Warning"), 596 JOptionPane.WARNING_MESSAGE 597 ); 598 } else { 599 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); 600 } 601 } 602 } 603 604 /** 605 * This class stores the result of the search in a local variable. 606 * @author Michael Zangl 607 */ 608 private static final class CapturingSearchReceiver implements SearchReceiver { 609 private Collection<OsmPrimitive> result; 610 611 @Override 612 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, 613 SearchSetting setting) { 614 this.result = result; 615 } 616 } 617 618 static final class SearchTask extends PleaseWaitRunnable { 619 private final DataSet ds; 620 private final SearchSetting setting; 621 private final Collection<OsmPrimitive> selection; 622 private final Predicate<OsmPrimitive> predicate; 623 private boolean canceled; 624 private int foundMatches; 625 private SearchReceiver resultReceiver; 626 627 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate, 628 SearchReceiver resultReceiver) { 629 super(tr("Searching")); 630 this.ds = ds; 631 this.setting = setting; 632 this.selection = selection; 633 this.predicate = predicate; 634 this.resultReceiver = resultReceiver; 635 } 636 637 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) { 638 final DataSet ds = Main.getLayerManager().getEditDataSet(); 639 return newSearchTask(setting, ds, resultReceiver); 640 } 641 642 /** 643 * Create a new search task for the given search setting. 644 * @param setting The setting to use 645 * @param ds The data set to search on 646 * @param resultReceiver will receive the search result 647 * @return A new search task. 648 */ 649 private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) { 650 final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected()); 651 return new SearchTask(ds, setting, selection, new Predicate<OsmPrimitive>() { 652 @Override 653 public boolean evaluate(OsmPrimitive o) { 654 return ds.isSelected(o); 655 } 656 }, resultReceiver); 657 } 658 659 @Override 660 protected void cancel() { 661 this.canceled = true; 662 } 663 664 @Override 665 protected void realRun() { 666 try { 667 foundMatches = 0; 668 SearchCompiler.Match matcher = SearchCompiler.compile(setting); 669 670 if (setting.mode == SearchMode.replace) { 671 selection.clear(); 672 } else if (setting.mode == SearchMode.in_selection) { 673 foundMatches = selection.size(); 674 } 675 676 Collection<OsmPrimitive> all; 677 if (setting.allElements) { 678 all = Main.getLayerManager().getEditDataSet().allPrimitives(); 679 } else { 680 all = Main.getLayerManager().getEditDataSet().allNonDeletedCompletePrimitives(); 681 } 682 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false); 683 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size())); 684 685 for (OsmPrimitive osm : all) { 686 if (canceled) { 687 return; 688 } 689 if (setting.mode == SearchMode.replace) { 690 if (matcher.match(osm)) { 691 selection.add(osm); 692 ++foundMatches; 693 } 694 } else if (setting.mode == SearchMode.add && !predicate.evaluate(osm) && matcher.match(osm)) { 695 selection.add(osm); 696 ++foundMatches; 697 } else if (setting.mode == SearchMode.remove && predicate.evaluate(osm) && matcher.match(osm)) { 698 selection.remove(osm); 699 ++foundMatches; 700 } else if (setting.mode == SearchMode.in_selection && predicate.evaluate(osm) && !matcher.match(osm)) { 701 selection.remove(osm); 702 --foundMatches; 703 } 704 subMonitor.worked(1); 705 } 706 subMonitor.finishTask(); 707 } catch (ParseError e) { 708 Main.debug(e); 709 JOptionPane.showMessageDialog( 710 Main.parent, 711 e.getMessage(), 712 tr("Error"), 713 JOptionPane.ERROR_MESSAGE 714 ); 715 } 716 } 717 718 @Override 719 protected void finish() { 720 if (canceled) { 721 return; 722 } 723 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting); 724 } 725 } 726 727 public static class SearchSetting { 728 public String text; 729 public SearchMode mode; 730 public boolean caseSensitive; 731 public boolean regexSearch; 732 public boolean mapCSSSearch; 733 public boolean allElements; 734 735 /** 736 * Constructs a new {@code SearchSetting}. 737 */ 738 public SearchSetting() { 739 text = ""; 740 mode = SearchMode.replace; 741 } 742 743 /** 744 * Constructs a new {@code SearchSetting} from an existing one. 745 * @param original original search settings 746 */ 747 public SearchSetting(SearchSetting original) { 748 text = original.text; 749 mode = original.mode; 750 caseSensitive = original.caseSensitive; 751 regexSearch = original.regexSearch; 752 mapCSSSearch = original.mapCSSSearch; 753 allElements = original.allElements; 754 } 755 756 @Override 757 public String toString() { 758 String cs = caseSensitive ? 759 /*case sensitive*/ trc("search", "CS") : 760 /*case insensitive*/ trc("search", "CI"); 761 String rx = regexSearch ? ", " + 762 /*regex search*/ trc("search", "RX") : ""; 763 String css = mapCSSSearch ? ", " + 764 /*MapCSS search*/ trc("search", "CSS") : ""; 765 String all = allElements ? ", " + 766 /*all elements*/ trc("search", "A") : ""; 767 return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')'; 768 } 769 770 @Override 771 public boolean equals(Object other) { 772 if (this == other) return true; 773 if (other == null || getClass() != other.getClass()) return false; 774 SearchSetting that = (SearchSetting) other; 775 return caseSensitive == that.caseSensitive && 776 regexSearch == that.regexSearch && 777 mapCSSSearch == that.mapCSSSearch && 778 allElements == that.allElements && 779 Objects.equals(text, that.text) && 780 mode == that.mode; 781 } 782 783 @Override 784 public int hashCode() { 785 return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements); 786 } 787 788 public static SearchSetting readFromString(String s) { 789 if (s.isEmpty()) 790 return null; 791 792 SearchSetting result = new SearchSetting(); 793 794 int index = 1; 795 796 result.mode = SearchMode.fromCode(s.charAt(0)); 797 if (result.mode == null) { 798 result.mode = SearchMode.replace; 799 index = 0; 800 } 801 802 while (index < s.length()) { 803 if (s.charAt(index) == 'C') { 804 result.caseSensitive = true; 805 } else if (s.charAt(index) == 'R') { 806 result.regexSearch = true; 807 } else if (s.charAt(index) == 'A') { 808 result.allElements = true; 809 } else if (s.charAt(index) == 'M') { 810 result.mapCSSSearch = true; 811 } else if (s.charAt(index) == ' ') { 812 break; 813 } else { 814 Main.warn("Unknown char in SearchSettings: " + s); 815 break; 816 } 817 index++; 818 } 819 820 if (index < s.length() && s.charAt(index) == ' ') { 821 index++; 822 } 823 824 result.text = s.substring(index); 825 826 return result; 827 } 828 829 public String writeToString() { 830 if (text == null || text.isEmpty()) 831 return ""; 832 833 StringBuilder result = new StringBuilder(); 834 result.append(mode.getCode()); 835 if (caseSensitive) { 836 result.append('C'); 837 } 838 if (regexSearch) { 839 result.append('R'); 840 } 841 if (mapCSSSearch) { 842 result.append('M'); 843 } 844 if (allElements) { 845 result.append('A'); 846 } 847 result.append(' ') 848 .append(text); 849 return result.toString(); 850 } 851 } 852 853 /** 854 * Refreshes the enabled state 855 * 856 */ 857 @Override 858 protected void updateEnabledState() { 859 setEnabled(getLayerManager().getEditLayer() != null); 860 } 861 862 @Override 863 public List<ActionParameter<?>> getActionParameters() { 864 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); 865 } 866}