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

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

SearchAction: Use ExpertToggleAction to determine if expert mode is active.

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