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                        JOptionPane.showMessageDialog(
354                                Main.parent,
355                                tr("Search expression is not valid: \n\n {0}", e.getMessage()),
356                                tr("Invalid search expression"),
357                                JOptionPane.ERROR_MESSAGE);
358                    }
359                } else {
360                    super.buttonAction(buttonIndex, evt);
361                }
362            }
363        };
364        dialog.setButtonIcons(new String[] {"dialogs/search", "cancel"});
365        dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
366        dialog.setContent(p);
367        dialog.showDialog();
368        int result = dialog.getValue();
369
370        if (result != 1) return null;
371
372        // User pressed OK - let's perform the search
373        SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace
374                : (add.isSelected() ? SearchAction.SearchMode.add
375                        : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection));
376        initialValues.text = hcbSearchString.getText();
377        initialValues.mode = mode;
378        initialValues.caseSensitive = caseSensitive.isSelected();
379        initialValues.allElements = allElements.isSelected();
380        initialValues.regexSearch = regexSearch.isSelected();
381        initialValues.mapCSSSearch = mapCSSSearch.isSelected();
382
383        if (addOnToolbar.isSelected()) {
384            ToolbarPreferences.ActionDefinition aDef =
385                    new ToolbarPreferences.ActionDefinition(Main.main.menu.search);
386            aDef.getParameters().put(SEARCH_EXPRESSION, initialValues);
387            // Display search expression as tooltip instead of generic one
388            aDef.setName(Utils.shortenString(initialValues.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY));
389            // parametrized action definition is now composed
390            ActionParser actionParser = new ToolbarPreferences.ActionParser(null);
391            String res = actionParser.saveAction(aDef);
392
393            // add custom search button to toolbar preferences
394            Main.toolbar.addCustomButton(res, -1, false);
395        }
396        return initialValues;
397    }
398
399    private static void buildHints(JPanel right, HistoryComboBox hcbSearchString) {
400        right.add(new SearchKeywordRow(hcbSearchString)
401                .addTitle(tr("basic examples"))
402                .addKeyword(tr("Baker Street"), null, tr("''Baker'' and ''Street'' in any key"))
403                .addKeyword(tr("\"Baker Street\""), "\"\"", tr("''Baker Street'' in any key")),
404                GBC.eol());
405        right.add(new SearchKeywordRow(hcbSearchString)
406                .addTitle(tr("basics"))
407                .addKeyword("<i>key</i>:<i>valuefragment</i>", null,
408                        tr("''valuefragment'' anywhere in ''key''"), "name:str matches name=Bakerstreet")
409                .addKeyword("-<i>key</i>:<i>valuefragment</i>", null, tr("''valuefragment'' nowhere in ''key''"))
410                .addKeyword("<i>key</i>=<i>value</i>", null, tr("''key'' with exactly ''value''"))
411                .addKeyword("<i>key</i>=*", null, tr("''key'' with any value"))
412                .addKeyword("*=<i>value</i>", null, tr("''value'' in any key"))
413                .addKeyword("<i>key</i>=", null, tr("matches if ''key'' exists"))
414                .addKeyword("<i>key</i>><i>value</i>", null, tr("matches if ''key'' is greater than ''value'' (analogously, less than)"))
415                .addKeyword("\"key\"=\"value\"", "\"\"=\"\"",
416                        tr("to quote operators.<br>Within quoted strings the <b>\"</b> and <b>\\</b> characters need to be escaped " +
417                           "by a preceding <b>\\</b> (e.g. <b>\\\"</b> and <b>\\\\</b>)."),
418                        "\"addr:street\""),
419                GBC.eol());
420        right.add(new SearchKeywordRow(hcbSearchString)
421                .addTitle(tr("combinators"))
422                .addKeyword("<i>expr</i> <i>expr</i>", null, tr("logical and (both expressions have to be satisfied)"))
423                .addKeyword("<i>expr</i> | <i>expr</i>", "| ", tr("logical or (at least one expression has to be satisfied)"))
424                .addKeyword("<i>expr</i> OR <i>expr</i>", "OR ", tr("logical or (at least one expression has to be satisfied)"))
425                .addKeyword("-<i>expr</i>", null, tr("logical not"))
426                .addKeyword("(<i>expr</i>)", "()", tr("use parenthesis to group expressions")),
427                GBC.eol());
428
429        if (Main.pref.getBoolean("expert", false)) {
430            right.add(new SearchKeywordRow(hcbSearchString)
431                .addTitle(tr("objects"))
432                .addKeyword("type:node", "type:node ", tr("all ways"))
433                .addKeyword("type:way", "type:way ", tr("all ways"))
434                .addKeyword("type:relation", "type:relation ", tr("all relations"))
435                .addKeyword("closed", "closed ", tr("all closed ways"))
436                .addKeyword("untagged", "untagged ", tr("object without useful tags")),
437                GBC.eol());
438            right.add(new SearchKeywordRow(hcbSearchString)
439                .addTitle(tr("metadata"))
440                .addKeyword("user:", "user:", tr("objects changed by user", "user:anonymous"))
441                .addKeyword("id:", "id:", tr("objects with given ID"), "id:0 (new objects)")
442                .addKeyword("version:", "version:", tr("objects with given version"), "version:0 (objects without an assigned version)")
443                .addKeyword("changeset:", "changeset:", tr("objects with given changeset ID"),
444                        "changeset:0 (objects without an assigned changeset)")
445                .addKeyword("timestamp:", "timestamp:", tr("objects with last modification timestamp within range"), "timestamp:2012/",
446                        "timestamp:2008/2011-02-04T12"),
447                GBC.eol());
448            right.add(new SearchKeywordRow(hcbSearchString)
449                .addTitle(tr("properties"))
450                .addKeyword("nodes:<i>20-</i>", "nodes:", tr("ways with at least 20 nodes, or relations containing at least 20 nodes"))
451                .addKeyword("ways:<i>3-</i>", "ways:", tr("nodes with at least 3 referring ways, or relations containing at least 3 ways"))
452                .addKeyword("tags:<i>5-10</i>", "tags:", tr("objects having 5 to 10 tags"))
453                .addKeyword("role:", "role:", tr("objects with given role in a relation"))
454                .addKeyword("areasize:<i>-100</i>", "areasize:", tr("closed ways with an area of 100 m\u00b2"))
455                .addKeyword("waylength:<i>200-</i>", "waylength:", tr("ways with a length of 200 m or more")),
456                GBC.eol());
457            right.add(new SearchKeywordRow(hcbSearchString)
458                .addTitle(tr("state"))
459                .addKeyword("modified", "modified ", tr("all modified objects"))
460                .addKeyword("new", "new ", tr("all new objects"))
461                .addKeyword("selected", "selected ", tr("all selected objects"))
462                .addKeyword("incomplete", "incomplete ", tr("all incomplete objects")),
463                GBC.eol());
464            right.add(new SearchKeywordRow(hcbSearchString)
465                .addTitle(tr("related objects"))
466                .addKeyword("child <i>expr</i>", "child ", tr("all children of objects matching the expression"), "child building")
467                .addKeyword("parent <i>expr</i>", "parent ", tr("all parents of objects matching the expression"), "parent bus_stop")
468                .addKeyword("hasRole:<i>stop</i>", "hasRole:", tr("relation containing a member of role <i>stop</i>"))
469                .addKeyword("role:<i>stop</i>", "role:", tr("objects being part of a relation as role <i>stop</i>"))
470                .addKeyword("nth:<i>7</i>", "nth:",
471                        tr("n-th member of relation and/or n-th node of way"), "nth:5 (child type:relation)", "nth:-1")
472                .addKeyword("nth%:<i>7</i>", "nth%:",
473                        tr("every n-th member of relation and/or every n-th node of way"), "nth%:100 (child waterway)"),
474                GBC.eol());
475            right.add(new SearchKeywordRow(hcbSearchString)
476                .addTitle(tr("view"))
477                .addKeyword("inview", "inview ", tr("objects in current view"))
478                .addKeyword("allinview", "allinview ", tr("objects (and all its way nodes / relation members) in current view"))
479                .addKeyword("indownloadedarea", "indownloadedarea ", tr("objects in downloaded area"))
480                .addKeyword("allindownloadedarea", "allindownloadedarea ",
481                        tr("objects (and all its way nodes / relation members) in downloaded area")),
482                GBC.eol());
483        }
484    }
485
486    /**
487     * Launches the dialog for specifying search criteria and runs a search
488     */
489    public static void search() {
490        SearchSetting se = showSearchDialog(lastSearch);
491        if (se != null) {
492            searchWithHistory(se);
493        }
494    }
495
496    /**
497     * Adds the search specified by the settings in <code>s</code> to the
498     * search history and performs the search.
499     *
500     * @param s search settings
501     */
502    public static void searchWithHistory(SearchSetting s) {
503        saveToHistory(s);
504        lastSearch = new SearchSetting(s);
505        search(s);
506    }
507
508    /**
509     * Performs the search specified by the settings in <code>s</code> without saving it to search history.
510     *
511     * @param s search settings
512     */
513    public static void searchWithoutHistory(SearchSetting s) {
514        lastSearch = new SearchSetting(s);
515        search(s);
516    }
517
518    /**
519     * Performs the search specified by the search string {@code search} and the search mode {@code mode}.
520     *
521     * @param search the search string to use
522     * @param mode the search mode to use
523     */
524    public static void search(String search, SearchMode mode) {
525        final SearchSetting searchSetting = new SearchSetting();
526        searchSetting.text = search;
527        searchSetting.mode = mode;
528        search(searchSetting);
529    }
530
531    static void search(SearchSetting s) {
532        SearchTask.newSearchTask(s).run();
533    }
534
535    static final class SearchTask extends PleaseWaitRunnable {
536        private final DataSet ds;
537        private final SearchSetting setting;
538        private final Collection<OsmPrimitive> selection;
539        private final Predicate<OsmPrimitive> predicate;
540        private boolean canceled;
541        private int foundMatches;
542
543        private SearchTask(DataSet ds, SearchSetting setting, Collection<OsmPrimitive> selection, Predicate<OsmPrimitive> predicate) {
544            super(tr("Searching"));
545            this.ds = ds;
546            this.setting = setting;
547            this.selection = selection;
548            this.predicate = predicate;
549        }
550
551        static SearchTask newSearchTask(SearchSetting setting) {
552            final DataSet ds = Main.main.getCurrentDataSet();
553            final Collection<OsmPrimitive> selection = new HashSet<>(ds.getAllSelected());
554            return new SearchTask(ds, setting, selection, new Predicate<OsmPrimitive>() {
555                @Override
556                public boolean evaluate(OsmPrimitive o) {
557                    return ds.isSelected(o);
558                }
559            });
560        }
561
562        @Override
563        protected void cancel() {
564            this.canceled = true;
565        }
566
567        @Override
568        protected void realRun() {
569            try {
570                foundMatches = 0;
571                SearchCompiler.Match matcher = SearchCompiler.compile(setting);
572
573                if (setting.mode == SearchMode.replace) {
574                    selection.clear();
575                } else if (setting.mode == SearchMode.in_selection) {
576                    foundMatches = selection.size();
577                }
578
579                Collection<OsmPrimitive> all;
580                if (setting.allElements) {
581                    all = Main.main.getCurrentDataSet().allPrimitives();
582                } else {
583                    all = Main.main.getCurrentDataSet().allNonDeletedCompletePrimitives();
584                }
585                final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false);
586                subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size()));
587
588                for (OsmPrimitive osm : all) {
589                    if (canceled) {
590                        return;
591                    }
592                    if (setting.mode == SearchMode.replace) {
593                        if (matcher.match(osm)) {
594                            selection.add(osm);
595                            ++foundMatches;
596                        }
597                    } else if (setting.mode == SearchMode.add && !predicate.evaluate(osm) && matcher.match(osm)) {
598                        selection.add(osm);
599                        ++foundMatches;
600                    } else if (setting.mode == SearchMode.remove && predicate.evaluate(osm) && matcher.match(osm)) {
601                        selection.remove(osm);
602                        ++foundMatches;
603                    } else if (setting.mode == SearchMode.in_selection && predicate.evaluate(osm) && !matcher.match(osm)) {
604                        selection.remove(osm);
605                        --foundMatches;
606                    }
607                    subMonitor.worked(1);
608                }
609                subMonitor.finishTask();
610            } catch (SearchCompiler.ParseError e) {
611                JOptionPane.showMessageDialog(
612                        Main.parent,
613                        e.getMessage(),
614                        tr("Error"),
615                        JOptionPane.ERROR_MESSAGE
616
617                );
618            }
619        }
620
621        @Override
622        protected void finish() {
623            if (canceled) {
624                return;
625            }
626            ds.setSelected(selection);
627            if (foundMatches == 0) {
628                final String msg;
629                final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY);
630                if (setting.mode == SearchMode.replace) {
631                    msg = tr("No match found for ''{0}''", text);
632                } else if (setting.mode == SearchMode.add) {
633                    msg = tr("Nothing added to selection by searching for ''{0}''", text);
634                } else if (setting.mode == SearchMode.remove) {
635                    msg = tr("Nothing removed from selection by searching for ''{0}''", text);
636                } else if (setting.mode == SearchMode.in_selection) {
637                    msg = tr("Nothing found in selection by searching for ''{0}''", text);
638                } else {
639                    msg = null;
640                }
641                Main.map.statusLine.setHelpText(msg);
642                JOptionPane.showMessageDialog(
643                        Main.parent,
644                        msg,
645                        tr("Warning"),
646                        JOptionPane.WARNING_MESSAGE
647                );
648            } else {
649                Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
650            }
651        }
652    }
653
654    public static class SearchSetting {
655        public String text;
656        public SearchMode mode;
657        public boolean caseSensitive;
658        public boolean regexSearch;
659        public boolean mapCSSSearch;
660        public boolean allElements;
661
662        /**
663         * Constructs a new {@code SearchSetting}.
664         */
665        public SearchSetting() {
666            text = "";
667            mode = SearchMode.replace;
668        }
669
670        /**
671         * Constructs a new {@code SearchSetting} from an existing one.
672         * @param original original search settings
673         */
674        public SearchSetting(SearchSetting original) {
675            text = original.text;
676            mode = original.mode;
677            caseSensitive = original.caseSensitive;
678            regexSearch = original.regexSearch;
679            mapCSSSearch = original.mapCSSSearch;
680            allElements = original.allElements;
681        }
682
683        @Override
684        public String toString() {
685            String cs = caseSensitive ?
686                    /*case sensitive*/  trc("search", "CS") :
687                        /*case insensitive*/  trc("search", "CI");
688            String rx = regexSearch ? ", " +
689                            /*regex search*/ trc("search", "RX") : "";
690            String css = mapCSSSearch ? ", " +
691                            /*MapCSS search*/ trc("search", "CSS") : "";
692            String all = allElements ? ", " +
693                            /*all elements*/ trc("search", "A") : "";
694            return '"' + text + "\" (" + cs + rx + css + all + ", " + mode + ')';
695        }
696
697        @Override
698        public boolean equals(Object other) {
699            if (this == other) return true;
700            if (other == null || getClass() != other.getClass()) return false;
701            SearchSetting that = (SearchSetting) other;
702            return caseSensitive == that.caseSensitive &&
703                    regexSearch == that.regexSearch &&
704                    mapCSSSearch == that.mapCSSSearch &&
705                    allElements == that.allElements &&
706                    Objects.equals(text, that.text) &&
707                    mode == that.mode;
708        }
709
710        @Override
711        public int hashCode() {
712            return Objects.hash(text, mode, caseSensitive, regexSearch, mapCSSSearch, allElements);
713        }
714
715        public static SearchSetting readFromString(String s) {
716            if (s.isEmpty())
717                return null;
718
719            SearchSetting result = new SearchSetting();
720
721            int index = 1;
722
723            result.mode = SearchMode.fromCode(s.charAt(0));
724            if (result.mode == null) {
725                result.mode = SearchMode.replace;
726                index = 0;
727            }
728
729            while (index < s.length()) {
730                if (s.charAt(index) == 'C') {
731                    result.caseSensitive = true;
732                } else if (s.charAt(index) == 'R') {
733                    result.regexSearch = true;
734                } else if (s.charAt(index) == 'A') {
735                    result.allElements = true;
736                } else if (s.charAt(index) == 'M') {
737                    result.mapCSSSearch = true;
738                } else if (s.charAt(index) == ' ') {
739                    break;
740                } else {
741                    Main.warn("Unknown char in SearchSettings: " + s);
742                    break;
743                }
744                index++;
745            }
746
747            if (index < s.length() && s.charAt(index) == ' ') {
748                index++;
749            }
750
751            result.text = s.substring(index);
752
753            return result;
754        }
755
756        public String writeToString() {
757            if (text == null || text.isEmpty())
758                return "";
759
760            StringBuilder result = new StringBuilder();
761            result.append(mode.getCode());
762            if (caseSensitive) {
763                result.append('C');
764            }
765            if (regexSearch) {
766                result.append('R');
767            }
768            if (mapCSSSearch) {
769                result.append('M');
770            }
771            if (allElements) {
772                result.append('A');
773            }
774            result.append(' ')
775                  .append(text);
776            return result.toString();
777        }
778    }
779
780    /**
781     * Refreshes the enabled state
782     *
783     */
784    @Override
785    protected void updateEnabledState() {
786        setEnabled(getEditLayer() != null);
787    }
788
789    @Override
790    public List<ActionParameter<?>> getActionParameters() {
791        return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
792    }
793}