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