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

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

see #15182 - move SearchCompiler from actions.search to data.osm.search

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