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