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

Last change on this file since 12279 was 12279, checked in by Don-vip, 7 years ago

sonar - squid:S3878 - Arrays should not be created for varargs parameters

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