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