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