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