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; 028import java.util.function.Predicate; 029 030import javax.swing.ButtonGroup; 031import javax.swing.JCheckBox; 032import javax.swing.JLabel; 033import javax.swing.JOptionPane; 034import javax.swing.JPanel; 035import javax.swing.JRadioButton; 036import javax.swing.text.BadLocationException; 037import javax.swing.text.JTextComponent; 038 039import org.openstreetmap.josm.Main; 040import org.openstreetmap.josm.actions.ActionParameter; 041import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter; 042import org.openstreetmap.josm.actions.JosmAction; 043import org.openstreetmap.josm.actions.ParameterizedAction; 044import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; 045import org.openstreetmap.josm.data.osm.DataSet; 046import org.openstreetmap.josm.data.osm.Filter; 047import org.openstreetmap.josm.data.osm.OsmPrimitive; 048import org.openstreetmap.josm.gui.ExtendedDialog; 049import org.openstreetmap.josm.gui.PleaseWaitRunnable; 050import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException; 051import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 052import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser; 053import org.openstreetmap.josm.gui.progress.ProgressMonitor; 054import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator; 055import org.openstreetmap.josm.gui.widgets.HistoryComboBox; 056import org.openstreetmap.josm.tools.GBC; 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 * @since 10600 (functional interface) 558 */ 559 @FunctionalInterface 560 interface SearchReceiver { 561 /** 562 * Receive the search result 563 * @param ds The data set searched on. 564 * @param result The result collection, including the initial collection. 565 * @param foundMatches The number of matches added to the result. 566 * @param setting The setting used. 567 */ 568 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting); 569 } 570 571 /** 572 * Select the search result and display a status text for it. 573 */ 574 private static class SelectSearchReceiver implements SearchReceiver { 575 576 @Override 577 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) { 578 ds.setSelected(result); 579 if (foundMatches == 0) { 580 final String msg; 581 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY); 582 if (setting.mode == SearchMode.replace) { 583 msg = tr("No match found for ''{0}''", text); 584 } else if (setting.mode == SearchMode.add) { 585 msg = tr("Nothing added to selection by searching for ''{0}''", text); 586 } else if (setting.mode == SearchMode.remove) { 587 msg = tr("Nothing removed from selection by searching for ''{0}''", text); 588 } else if (setting.mode == SearchMode.in_selection) { 589 msg = tr("Nothing found in selection by searching for ''{0}''", text); 590 } else { 591 msg = null; 592 } 593 Main.map.statusLine.setHelpText(msg); 594 JOptionPane.showMessageDialog( 595 Main.parent, 596 msg, 597 tr("Warning"), 598 JOptionPane.WARNING_MESSAGE 599 ); 600 } else { 601 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); 602 } 603 } 604 } 605 606 /** 607 * This class stores the result of the search in a local variable. 608 * @author Michael Zangl 609 */ 610 private static final class CapturingSearchReceiver implements SearchReceiver { 611 private Collection<OsmPrimitive> result; 612 613 @Override 614 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, 615 SearchSetting setting) { 616 this.result = result; 617 } 618 } 619 620 static final class SearchTask extends PleaseWaitRunnable { 621 private final DataSet ds; 622 private final SearchSetting setting; 623 private final Collection<OsmPrimitive> selection; 624 private final Predicate<OsmPrimitive> predicate; 625 private boolean canceled; 626 private int foundMatches; 627 private final SearchReceiver resultReceiver; 628 629 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate, 630 SearchReceiver resultReceiver) { 631 super(tr("Searching")); 632 this.ds = ds; 633 this.setting = setting; 634 this.selection = selection; 635 this.predicate = predicate; 636 this.resultReceiver = resultReceiver; 637 } 638 639 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) { 640 final DataSet ds = Main.getLayerManager().getEditDataSet(); 641 return newSearchTask(setting, ds, resultReceiver); 642 } 643 644 /** 645 * Create a new search task for the given search setting. 646 * @param setting The setting to use 647 * @param ds The data set to search on 648 * @param resultReceiver will receive the search result 649 * @return A new search task. 650 */ 651 private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) { 652 final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected()); 653 return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver); 654 } 655 656 @Override 657 protected void cancel() { 658 this.canceled = true; 659 } 660 661 @Override 662 protected void realRun() { 663 try { 664 foundMatches = 0; 665 SearchCompiler.Match matcher = SearchCompiler.compile(setting); 666 667 if (setting.mode == SearchMode.replace) { 668 selection.clear(); 669 } else if (setting.mode == SearchMode.in_selection) { 670 foundMatches = selection.size(); 671 } 672 673 Collection<OsmPrimitive> all; 674 if (setting.allElements) { 675 all = ds.allPrimitives(); 676 } else { 677 all = ds.getPrimitives(OsmPrimitive::isSelectable); 678 } 679 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false); 680 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size())); 681 682 for (OsmPrimitive osm : all) { 683 if (canceled) { 684 return; 685 } 686 if (setting.mode == SearchMode.replace) { 687 if (matcher.match(osm)) { 688 selection.add(osm); 689 ++foundMatches; 690 } 691 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) { 692 selection.add(osm); 693 ++foundMatches; 694 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) { 695 selection.remove(osm); 696 ++foundMatches; 697 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) { 698 selection.remove(osm); 699 --foundMatches; 700 } 701 subMonitor.worked(1); 702 } 703 subMonitor.finishTask(); 704 } catch (ParseError e) { 705 Main.debug(e); 706 JOptionPane.showMessageDialog( 707 Main.parent, 708 e.getMessage(), 709 tr("Error"), 710 JOptionPane.ERROR_MESSAGE 711 ); 712 } 713 } 714 715 @Override 716 protected void finish() { 717 if (canceled) { 718 return; 719 } 720 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting); 721 } 722 } 723 724 public static class SearchSetting { 725 public String text; 726 public SearchMode mode; 727 public boolean caseSensitive; 728 public boolean regexSearch; 729 public boolean mapCSSSearch; 730 public boolean allElements; 731 732 /** 733 * Constructs a new {@code SearchSetting}. 734 */ 735 public SearchSetting() { 736 text = ""; 737 mode = SearchMode.replace; 738 } 739 740 /** 741 * Constructs a new {@code SearchSetting} from an existing one. 742 * @param original original search settings 743 */ 744 public SearchSetting(SearchSetting original) { 745 text = original.text; 746 mode = original.mode; 747 caseSensitive = original.caseSensitive; 748 regexSearch = original.regexSearch; 749 mapCSSSearch = original.mapCSSSearch; 750 allElements = original.allElements; 751 } 752 753 @Override 754 public String toString() { 755 String cs = caseSensitive ? 756 /*case sensitive*/ trc("search", "CS") : 757 /*case insensitive*/ trc("search", "CI"); 758 String rx = regexSearch ? ", " + 759 /*regex search*/ trc("search", "RX") : ""; 760 String css = mapCSSSearch ? ", " + 761 /*MapCSS search*/ trc("search", "CSS") : ""; 762 String all = allElements ? ", " + 763 /*all elements*/ trc("search", "A") : ""; 764 return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')'; 765 } 766 767 @Override 768 public boolean equals(Object other) { 769 if (this == other) return true; 770 if (other == null || getClass() != other.getClass()) return false; 771 SearchSetting that = (SearchSetting) other; 772 return caseSensitive == that.caseSensitive && 773 regexSearch == that.regexSearch && 774 mapCSSSearch == that.mapCSSSearch && 775 allElements == that.allElements && 776 Objects.equals(text, that.text) && 777 mode == that.mode; 778 } 779 780 @Override 781 public int hashCode() { 782 return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements); 783 } 784 785 public static SearchSetting readFromString(String s) { 786 if (s.isEmpty()) 787 return null; 788 789 SearchSetting result = new SearchSetting(); 790 791 int index = 1; 792 793 result.mode = SearchMode.fromCode(s.charAt(0)); 794 if (result.mode == null) { 795 result.mode = SearchMode.replace; 796 index = 0; 797 } 798 799 while (index < s.length()) { 800 if (s.charAt(index) == 'C') { 801 result.caseSensitive = true; 802 } else if (s.charAt(index) == 'R') { 803 result.regexSearch = true; 804 } else if (s.charAt(index) == 'A') { 805 result.allElements = true; 806 } else if (s.charAt(index) == 'M') { 807 result.mapCSSSearch = true; 808 } else if (s.charAt(index) == ' ') { 809 break; 810 } else { 811 Main.warn("Unknown char in SearchSettings: " + s); 812 break; 813 } 814 index++; 815 } 816 817 if (index < s.length() && s.charAt(index) == ' ') { 818 index++; 819 } 820 821 result.text = s.substring(index); 822 823 return result; 824 } 825 826 public String writeToString() { 827 if (text == null || text.isEmpty()) 828 return ""; 829 830 StringBuilder result = new StringBuilder(); 831 result.append(mode.getCode()); 832 if (caseSensitive) { 833 result.append('C'); 834 } 835 if (regexSearch) { 836 result.append('R'); 837 } 838 if (mapCSSSearch) { 839 result.append('M'); 840 } 841 if (allElements) { 842 result.append('A'); 843 } 844 result.append(' ') 845 .append(text); 846 return result.toString(); 847 } 848 } 849 850 /** 851 * Refreshes the enabled state 852 * 853 */ 854 @Override 855 protected void updateEnabledState() { 856 setEnabled(getLayerManager().getEditLayer() != null); 857 } 858 859 @Override 860 public List<ActionParameter<?>> getActionParameters() { 861 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); 862 } 863}