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