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