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;
028
029import javax.swing.ButtonGroup;
030import javax.swing.JCheckBox;
031import javax.swing.JLabel;
032import javax.swing.JOptionPane;
033import javax.swing.JPanel;
034import javax.swing.JRadioButton;
035import javax.swing.text.BadLocationException;
036import javax.swing.text.JTextComponent;
037
038import org.openstreetmap.josm.Main;
039import org.openstreetmap.josm.actions.ActionParameter;
040import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
041import org.openstreetmap.josm.actions.JosmAction;
042import org.openstreetmap.josm.actions.ParameterizedAction;
043import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
044import org.openstreetmap.josm.data.osm.DataSet;
045import org.openstreetmap.josm.data.osm.Filter;
046import org.openstreetmap.josm.data.osm.OsmPrimitive;
047import org.openstreetmap.josm.gui.ExtendedDialog;
048import org.openstreetmap.josm.gui.PleaseWaitRunnable;
049import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSException;
050import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
051import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser;
052import org.openstreetmap.josm.gui.progress.ProgressMonitor;
053import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
054import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
055import org.openstreetmap.josm.tools.GBC;
056import org.openstreetmap.josm.tools.Predicate;
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     */
558    interface SearchReceiver {
559        /**
560         * Receive the search result
561         * @param ds The data set searched on.
562         * @param result The result collection, including the initial collection.
563         * @param foundMatches The number of matches added to the result.
564         * @param setting The setting used.
565         */
566        void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting);
567    }
568
569    /**
570     * Select the search result and display a status text for it.
571     */
572    private static class SelectSearchReceiver implements SearchReceiver {
573
574        @Override
575        public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches, SearchSetting setting) {
576            ds.setSelected(result);
577            if (foundMatches == 0) {
578                final String msg;
579                final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
580                if (setting.mode == SearchMode.replace) {
581                    msg = tr("No match found for ''{0}''", text);
582                } else if (setting.mode == SearchMode.add) {
583                    msg = tr("Nothing added to selection by searching for ''{0}''", text);
584                } else if (setting.mode == SearchMode.remove) {
585                    msg = tr("Nothing removed from selection by searching for ''{0}''", text);
586                } else if (setting.mode == SearchMode.in_selection) {
587                    msg = tr("Nothing found in selection by searching for ''{0}''", text);
588                } else {
589                    msg = null;
590                }
591                Main.map.statusLine.setHelpText(msg);
592                JOptionPane.showMessageDialog(
593                        Main.parent,
594                        msg,
595                        tr("Warning"),
596                        JOptionPane.WARNING_MESSAGE
597                );
598            } else {
599                Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
600            }
601        }
602    }
603
604    /**
605     * This class stores the result of the search in a local variable.
606     * @author Michael Zangl
607     */
608    private static final class CapturingSearchReceiver implements SearchReceiver {
609        private Collection<OsmPrimitive> result;
610
611        @Override
612        public void receiveSearchResult(DataSet ds, Collection<OsmPrimitive> result, int foundMatches,
613                SearchSetting setting) {
614                    this.result = result;
615        }
616    }
617
618    static final class SearchTask extends PleaseWaitRunnable {
619        private final DataSet ds;
620        private final SearchSetting setting;
621        private final Collection<OsmPrimitive> selection;
622        private final Predicate<OsmPrimitive> predicate;
623        private boolean canceled;
624        private int foundMatches;
625        private SearchReceiver resultReceiver;
626
627        private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate,
628                SearchReceiver resultReceiver) {
629            super(tr("Searching"));
630            this.ds = ds;
631            this.setting = setting;
632            this.selection = selection;
633            this.predicate = predicate;
634            this.resultReceiver = resultReceiver;
635        }
636
637        static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) {
638            final DataSet ds = Main.getLayerManager().getEditDataSet();
639            return newSearchTask(setting, ds, resultReceiver);
640        }
641
642        /**
643         * Create a new search task for the given search setting.
644         * @param setting The setting to use
645         * @param ds The data set to search on
646         * @param resultReceiver will receive the search result
647         * @return A new search task.
648         */
649        private static SearchTask newSearchTask(SearchSetting setting, final DataSet ds, SearchReceiver resultReceiver) {
650            final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
651            return new SearchTask(ds, setting, selection, new Predicate<OsmPrimitive>() {
652                @Override
653                public boolean evaluate(OsmPrimitive o) {
654                    return ds.isSelected(o);
655                }
656            }, resultReceiver);
657        }
658
659        @Override
660        protected void cancel() {
661            this.canceled = true;
662        }
663
664        @Override
665        protected void realRun() {
666            try {
667                foundMatches = 0;
668                SearchCompiler.Match matcher = SearchCompiler.compile(setting);
669
670                if (setting.mode == SearchMode.replace) {
671                    selection.clear();
672                } else if (setting.mode == SearchMode.in_selection) {
673                    foundMatches = selection.size();
674                }
675
676                Collection<OsmPrimitive> all;
677                if (setting.allElements) {
678                    all = Main.getLayerManager().getEditDataSet().allPrimitives();
679                } else {
680                    all = Main.getLayerManager().getEditDataSet().allNonDeletedCompletePrimitives();
681                }
682                final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
683                subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
684
685                for (OsmPrimitive osm : all) {
686                    if (canceled) {
687                        return;
688                    }
689                    if (setting.mode == SearchMode.replace) {
690                        if (matcher.match(osm)) {
691                            selection.add(osm);
692                            ++foundMatches;
693                        }
694                    } else if (setting.mode == SearchMode.add && !predicate.evaluate(osm) && matcher.match(osm)) {
695                        selection.add(osm);
696                        ++foundMatches;
697                    } else if (setting.mode == SearchMode.remove && predicate.evaluate(osm) && matcher.match(osm)) {
698                        selection.remove(osm);
699                        ++foundMatches;
700                    } else if (setting.mode == SearchMode.in_selection && predicate.evaluate(osm) && !matcher.match(osm)) {
701                        selection.remove(osm);
702                        --foundMatches;
703                    }
704                    subMonitor.worked(1);
705                }
706                subMonitor.finishTask();
707            } catch (ParseError e) {
708                Main.debug(e);
709                JOptionPane.showMessageDialog(
710                        Main.parent,
711                        e.getMessage(),
712                        tr("Error"),
713                        JOptionPane.ERROR_MESSAGE
714                );
715            }
716        }
717
718        @Override
719        protected void finish() {
720            if (canceled) {
721                return;
722            }
723            resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting);
724        }
725    }
726
727    public static class SearchSetting {
728        public String text;
729        public SearchMode mode;
730        public boolean caseSensitive;
731        public boolean regexSearch;
732        public boolean mapCSSSearch;
733        public boolean allElements;
734
735        /**
736         * Constructs a new {@code SearchSetting}.
737         */
738        public SearchSetting() {
739            text = "";
740            mode = SearchMode.replace;
741        }
742
743        /**
744         * Constructs a new {@code SearchSetting} from an existing one.
745         * @param original original search settings
746         */
747        public SearchSetting(SearchSetting original) {
748            text = original.text;
749            mode = original.mode;
750            caseSensitive = original.caseSensitive;
751            regexSearch = original.regexSearch;
752            mapCSSSearch = original.mapCSSSearch;
753            allElements = original.allElements;
754        }
755
756        @Override
757        public String toString() {
758            String cs = caseSensitive ?
759                    /*case sensitive*/  trc("search", "CS") :
760                        /*case insensitive*/  trc("search", "CI");
761            String rx = regexSearch ? ", " +
762                            /*regex search*/ trc("search", "RX") : "";
763            String css = mapCSSSearch ? ", " +
764                            /*MapCSS search*/ trc("search", "CSS") : "";
765            String all = allElements ? ", " +
766                            /*all elements*/ trc("search", "A") : "";
767            return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
768        }
769
770        @Override
771        public boolean equals(Object other) {
772            if (this == other) return true;
773            if (other == null || getClass() != other.getClass()) return false;
774            SearchSetting that = (SearchSetting) other;
775            return caseSensitive == that.caseSensitive &&
776                    regexSearch == that.regexSearch &&
777                    mapCSSSearch == that.mapCSSSearch &&
778                    allElements == that.allElements &&
779                    Objects.equals(text, that.text) &&
780                    mode == that.mode;
781        }
782
783        @Override
784        public int hashCode() {
785            return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
786        }
787
788        public static SearchSetting readFromString(String s) {
789            if (s.isEmpty())
790                return null;
791
792            SearchSetting result = new SearchSetting();
793
794            int index = 1;
795
796            result.mode = SearchMode.fromCode(s.charAt(0));
797            if (result.mode == null) {
798                result.mode = SearchMode.replace;
799                index = 0;
800            }
801
802            while (index < s.length()) {
803                if (s.charAt(index) == 'C') {
804                    result.caseSensitive = true;
805                } else if (s.charAt(index) == 'R') {
806                    result.regexSearch = true;
807                } else if (s.charAt(index) == 'A') {
808                    result.allElements = true;
809                } else if (s.charAt(index) == 'M') {
810                    result.mapCSSSearch = true;
811                } else if (s.charAt(index) == ' ') {
812                    break;
813                } else {
814                    Main.warn("Unknown char in SearchSettings: " + s);
815                    break;
816                }
817                index++;
818            }
819
820            if (index < s.length() && s.charAt(index) == ' ') {
821                index++;
822            }
823
824            result.text = s.substring(index);
825
826            return result;
827        }
828
829        public String writeToString() {
830            if (text == null || text.isEmpty())
831                return "";
832
833            StringBuilder result = new StringBuilder();
834            result.append(mode.getCode());
835            if (caseSensitive) {
836                result.append('C');
837            }
838            if (regexSearch) {
839                result.append('R');
840            }
841            if (mapCSSSearch) {
842                result.append('M');
843            }
844            if (allElements) {
845                result.append('A');
846            }
847            result.append(' ')
848                  .append(text);
849            return result.toString();
850        }
851    }
852
853    /**
854     * Refreshes the enabled state
855     *
856     */
857    @Override
858    protected void updateEnabledState() {
859        setEnabled(getLayerManager().getEditLayer() != null);
860    }
861
862    @Override
863    public List<ActionParameter<?>> getActionParameters() {
864        return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
865    }
866}