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

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

see #11390 - sonar - squid:S1604 - Java 8: Anonymous inner classes containing only one method should become lambdas

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