source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java@ 11446

Last change on this file since 11446 was 11446, checked in by stoecker, 7 years ago

add search option to find deleted objects (e.g. to purge them)

  • Property svn:eol-style set to native
File size: 36.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.awt.Cursor;
10import java.awt.Dimension;
11import java.awt.FlowLayout;
12import java.awt.GridBagLayout;
13import java.awt.event.ActionEvent;
14import java.awt.event.KeyEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.util.ArrayList;
18import java.util.Arrays;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashSet;
22import java.util.LinkedHashSet;
23import java.util.LinkedList;
24import java.util.List;
25import java.util.Map;
26import java.util.Objects;
27import java.util.Set;
28import java.util.function.Predicate;
29
30import javax.swing.ButtonGroup;
31import javax.swing.JCheckBox;
32import javax.swing.JLabel;
33import javax.swing.JOptionPane;
34import javax.swing.JPanel;
35import javax.swing.JRadioButton;
36import javax.swing.text.BadLocationException;
37import javax.swing.text.JTextComponent;
38
39import org.openstreetmap.josm.Main;
40import org.openstreetmap.josm.actions.ActionParameter;
41import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
42import org.openstreetmap.josm.actions.JosmAction;
43import org.openstreetmap.josm.actions.ParameterizedAction;
44import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
45import org.openstreetmap.josm.data.osm.DataSet;
46import org.openstreetmap.josm.data.osm.Filter;
47import org.openstreetmap.josm.data.osm.OsmPrimitive;
48import org.openstreetmap.josm.gui.ExtendedDialog;
49import org.openstreetmap.josm.gui.PleaseWaitRunnable;
50import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
51import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
52import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
53import org.openstreetmap.josm.gui.progress.ProgressMonitor;
54import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
55import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
56import org.openstreetmap.josm.tools.GBC;
57import org.openstreetmap.josm.tools.JosmRuntimeException;
58import org.openstreetmap.josm.tools.Shortcut;
59import org.openstreetmap.josm.tools.Utils;
60
61public class SearchAction extends JosmAction implements ParameterizedAction {
62
63 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15;
64 /** Maximum number of characters before the search expression is shortened for display purposes. */
65 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100;
66
67 private static final String SEARCH_EXPRESSION = "searchExpression";
68
69 public enum SearchMode {
70 /** replace selection */
71 replace('R'),
72 /** add to selection */
73 add('A'),
74 /** remove from selection */
75 remove('D'),
76 /** find in selection */
77 in_selection('S');
78
79 private final char code;
80
81 SearchMode(char code) {
82 this.code = code;
83 }
84
85 /**
86 * Returns the unique character code of this mode.
87 * @return the unique character code of this mode
88 */
89 public char getCode() {
90 return code;
91 }
92
93 /**
94 * Returns the search mode matching the given character code.
95 * @param code character code
96 * @return search mode matching the given character code
97 */
98 public static SearchMode fromCode(char code) {
99 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 .addKeyword("deleted", "deleted ", tr("all deleted objects (checkbox <b>{0}</b> must be enabled)", tr("all objects"))),
466 GBC.eol());
467 right.add(new SearchKeywordRow(hcbSearchString)
468 .addTitle(tr("related objects"))
469 .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
470 .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
471 .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
472 .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
473 .addKeyword("nth:<i>7</i>", "nth:",
474 tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
475 .addKeyword("nth%:<i>7</i>", "nth%:",
476 tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
477 GBC.eol());
478 right.add(new SearchKeywordRow(hcbSearchString)
479 .addTitle(tr("view"))
480 .addKeyword("inview", "inview ", tr("objects in current view"))
481 .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
482 .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
483 .addKeyword("allindownloadedarea", "allindownloadedarea ",
484 tr("objects (and all its way nodes / relation members) in downloaded area")),
485 GBC.eol());
486 }
487 }
488
489 /**
490 * Launches the dialog for specifying search criteria and runs a search
491 */
492 public static void search() {
493 SearchSetting se = showSearchDialog(lastSearch);
494 if (se != null) {
495 searchWithHistory(se);
496 }
497 }
498
499 /**
500 * Adds the search specified by the settings in <code>s</code> to the
501 * search history and performs the search.
502 *
503 * @param s search settings
504 */
505 public static void searchWithHistory(SearchSetting s) {
506 saveToHistory(s);
507 lastSearch = new SearchSetting(s);
508 search(s);
509 }
510
511 /**
512 * Performs the search specified by the settings in <code>s</code> without saving it to search history.
513 *
514 * @param s search settings
515 */
516 public static void searchWithoutHistory(SearchSetting s) {
517 lastSearch = new SearchSetting(s);
518 search(s);
519 }
520
521 /**
522 * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
523 *
524 * @param search the search string to use
525 * @param mode the search mode to use
526 */
527 public static void search(String search, SearchMode mode) {
528 final SearchSetting searchSetting = new SearchSetting();
529 searchSetting.text = search;
530 searchSetting.mode = mode;
531 search(searchSetting);
532 }
533
534 static void search(SearchSetting s) {
535 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run();
536 }
537
538 /**
539 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search.
540 *
541 * @param search the search string to use
542 * @param mode the search mode to use
543 * @return The result of the search.
544 * @since 10457
545 */
546 public static Collection<OsmPrimitive> searchAndReturn(String search, SearchMode mode) {
547 final SearchSetting searchSetting = new SearchSetting();
548 searchSetting.text = search;
549 searchSetting.mode = mode;
550 CapturingSearchReceiver receiver = new CapturingSearchReceiver();
551 SearchTask.newSearchTask(searchSetting, receiver).run();
552 return receiver.result;
553 }
554
555 /**
556 * Interfaces implementing this may receive the result of the current search.
557 * @author Michael Zangl
558 * @since 10457
559 * @since 10600 (functional interface)
560 */
561 @FunctionalInterface
562 interface SearchReceiver {
563 /**
564 * Receive the search result
565 * @param ds The data set searched on.
566 * @param result The result collection, including the initial collection.
567 * @param foundMatches The number of matches added to the result.
568 * @param setting The setting used.
569 */
570 void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting);
571 }
572
573 /**
574 * Select the search result and display a status text for it.
575 */
576 private static class SelectSearchReceiver implements SearchReceiver {
577
578 @Override
579 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) {
580 ds.setSelected(result);
581 if (foundMatches == 0) {
582 final String msg;
583 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
584 if (setting.mode == SearchMode.replace) {
585 msg = tr("No match found for ''{0}''", text);
586 } else if (setting.mode == SearchMode.add) {
587 msg = tr("Nothing added to selection by searching for ''{0}''", text);
588 } else if (setting.mode == SearchMode.remove) {
589 msg = tr("Nothing removed from selection by searching for ''{0}''", text);
590 } else if (setting.mode == SearchMode.in_selection) {
591 msg = tr("Nothing found in selection by searching for ''{0}''", text);
592 } else {
593 msg = null;
594 }
595 Main.map.statusLine.setHelpText(msg);
596 JOptionPane.showMessageDialog(
597 Main.parent,
598 msg,
599 tr("Warning"),
600 JOptionPane.WARNING_MESSAGE
601 );
602 } else {
603 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
604 }
605 }
606 }
607
608 /**
609 * This class stores the result of the search in a local variable.
610 * @author Michael Zangl
611 */
612 private static final class CapturingSearchReceiver implements SearchReceiver {
613 private Collection<OsmPrimitive> result;
614
615 @Override
616 public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches,
617 SearchSetting setting) {
618 this.result = result;
619 }
620 }
621
622 static final class SearchTask extends PleaseWaitRunnable {
623 private final DataSet ds;
624 private final SearchSetting setting;
625 private final Collection<OsmPrimitive> selection;
626 private final Predicate<OsmPrimitive> predicate;
627 private boolean canceled;
628 private int foundMatches;
629 private final SearchReceiver resultReceiver;
630
631 private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
632 SearchReceiver resultReceiver) {
633 super(tr("Searching"));
634 this.ds = ds;
635 this.setting = setting;
636 this.selection = selection;
637 this.predicate = predicate;
638 this.resultReceiver = resultReceiver;
639 }
640
641 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
642 final DataSet ds = Main.getLayerManager().getEditDataSet();
643 return newSearchTask(setting, ds, resultReceiver);
644 }
645
646 /**
647 * Create a new search task for the given search setting.
648 * @param setting The setting to use
649 * @param ds The data set to search on
650 * @param resultReceiver will receive the search result
651 * @return A new search task.
652 */
653 private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
654 final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
655 return new SearchTask(ds, setting, selection, ds::isSelected, resultReceiver);
656 }
657
658 @Override
659 protected void cancel() {
660 this.canceled = true;
661 }
662
663 @Override
664 protected void realRun() {
665 try {
666 foundMatches = 0;
667 SearchCompiler.Match matcher = SearchCompiler.compile(setting);
668
669 if (setting.mode == SearchMode.replace) {
670 selection.clear();
671 } else if (setting.mode == SearchMode.in_selection) {
672 foundMatches = selection.size();
673 }
674
675 Collection<OsmPrimitive> all;
676 if (setting.allElements) {
677 all = ds.allPrimitives();
678 } else {
679 all = ds.getPrimitives(OsmPrimitive::isSelectable);
680 }
681 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
682 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
683
684 for (OsmPrimitive osm : all) {
685 if (canceled) {
686 return;
687 }
688 if (setting.mode == SearchMode.replace) {
689 if (matcher.match(osm)) {
690 selection.add(osm);
691 ++foundMatches;
692 }
693 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) {
694 selection.add(osm);
695 ++foundMatches;
696 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) {
697 selection.remove(osm);
698 ++foundMatches;
699 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) {
700 selection.remove(osm);
701 --foundMatches;
702 }
703 subMonitor.worked(1);
704 }
705 subMonitor.finishTask();
706 } catch (ParseError e) {
707 Main.debug(e);
708 JOptionPane.showMessageDialog(
709 Main.parent,
710 e.getMessage(),
711 tr("Error"),
712 JOptionPane.ERROR_MESSAGE
713 );
714 }
715 }
716
717 @Override
718 protected void finish() {
719 if (canceled) {
720 return;
721 }
722 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting);
723 }
724 }
725
726 public static class SearchSetting {
727 public String text;
728 public SearchMode mode;
729 public boolean caseSensitive;
730 public boolean regexSearch;
731 public boolean mapCSSSearch;
732 public boolean allElements;
733
734 /**
735 * Constructs a new {@code SearchSetting}.
736 */
737 public SearchSetting() {
738 text = "";
739 mode = SearchMode.replace;
740 }
741
742 /**
743 * Constructs a new {@code SearchSetting} from an existing one.
744 * @param original original search settings
745 */
746 public SearchSetting(SearchSetting original) {
747 text = original.text;
748 mode = original.mode;
749 caseSensitive = original.caseSensitive;
750 regexSearch = original.regexSearch;
751 mapCSSSearch = original.mapCSSSearch;
752 allElements = original.allElements;
753 }
754
755 @Override
756 public String toString() {
757 String cs = caseSensitive ?
758 /*case sensitive*/ trc("search", "CS") :
759 /*case insensitive*/ trc("search", "CI");
760 String rx = regexSearch ? ", " +
761 /*regex search*/ trc("search", "RX") : "";
762 String css = mapCSSSearch ? ", " +
763 /*MapCSS search*/ trc("search", "CSS") : "";
764 String all = allElements ? ", " +
765 /*all elements*/ trc("search", "A") : "";
766 return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
767 }
768
769 @Override
770 public boolean equals(Object other) {
771 if (this == other) return true;
772 if (other == null || getClass() != other.getClass()) return false;
773 SearchSetting that = (SearchSetting) other;
774 return caseSensitive == that.caseSensitive &&
775 regexSearch == that.regexSearch &&
776 mapCSSSearch == that.mapCSSSearch &&
777 allElements == that.allElements &&
778 Objects.equals(text, that.text) &&
779 mode == that.mode;
780 }
781
782 @Override
783 public int hashCode() {
784 return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
785 }
786
787 public static SearchSetting readFromString(String s) {
788 if (s.isEmpty())
789 return null;
790
791 SearchSetting result = new SearchSetting();
792
793 int index = 1;
794
795 result.mode = SearchMode.fromCode(s.charAt(0));
796 if (result.mode == null) {
797 result.mode = SearchMode.replace;
798 index = 0;
799 }
800
801 while (index < s.length()) {
802 if (s.charAt(index) == 'C') {
803 result.caseSensitive = true;
804 } else if (s.charAt(index) == 'R') {
805 result.regexSearch = true;
806 } else if (s.charAt(index) == 'A') {
807 result.allElements = true;
808 } else if (s.charAt(index) == 'M') {
809 result.mapCSSSearch = true;
810 } else if (s.charAt(index) == ' ') {
811 break;
812 } else {
813 Main.warn("Unknown char in SearchSettings: " + s);
814 break;
815 }
816 index++;
817 }
818
819 if (index < s.length() && s.charAt(index) == ' ') {
820 index++;
821 }
822
823 result.text = s.substring(index);
824
825 return result;
826 }
827
828 public String writeToString() {
829 if (text == null || text.isEmpty())
830 return "";
831
832 StringBuilder result = new StringBuilder();
833 result.append(mode.getCode());
834 if (caseSensitive) {
835 result.append('C');
836 }
837 if (regexSearch) {
838 result.append('R');
839 }
840 if (mapCSSSearch) {
841 result.append('M');
842 }
843 if (allElements) {
844 result.append('A');
845 }
846 result.append(' ')
847 .append(text);
848 return result.toString();
849 }
850 }
851
852 /**
853 * Refreshes the enabled state
854 *
855 */
856 @Override
857 protected void updateEnabledState() {
858 setEnabled(getLayerManager().getEditLayer() != null);
859 }
860
861 @Override
862 public List<ActionParameter<?>> getActionParameters() {
863 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
864 }
865}
Note: See TracBrowser for help on using the repository browser.