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