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

Last change on this file since 12334 was 12334, checked in by michael2402, 7 years ago

Javadoc for SearchAction.

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