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