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

Last change on this file since 11396 was 11374, checked in by Don-vip, 8 years ago

sonar - squid:S00112 - Generic exceptions should never be thrown: define JosmRuntimeException

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