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, 21 months 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.