// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.actions.search; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trc; import java.awt.Font; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.ButtonGroup; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JRadioButton; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.ActionParameter; import org.openstreetmap.josm.actions.JosmAction; import org.openstreetmap.josm.actions.ParameterizedAction; import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.Filter; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.gui.ExtendedDialog; import org.openstreetmap.josm.gui.widgets.HistoryComboBox; import org.openstreetmap.josm.tools.GBC; import org.openstreetmap.josm.tools.Shortcut; public class SearchAction extends JosmAction implements ParameterizedAction { public static final int DEFAULT_SEARCH_HISTORY_SIZE = 10; private static final String SEARCH_EXPRESSION = "searchExpression"; public static enum SearchMode { replace('R'), add('A'), remove('D'), in_selection('S'); private final char code; SearchMode(char code) { this.code = code; } public char getCode() { return code; } public static SearchMode fromCode(char code) { for (SearchMode mode: values()) { if (mode.getCode() == code) return mode; } return null; } } private static LinkedList<SearchSetting> searchHistory = null; public static Collection<SearchSetting> getSearchHistory() { if (searchHistory == null) { searchHistory = new LinkedList<SearchSetting>(); for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) { SearchSetting ss = SearchSetting.readFromString(s); if (ss != null) { searchHistory.add(ss); } } } return searchHistory; } public static void saveToHistory(SearchSetting s) { if(searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) { searchHistory.addFirst(new SearchSetting(s)); } int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE); while (searchHistory.size() > maxsize) { searchHistory.removeLast(); } List<String> savedHistory = new ArrayList<String>(); for (SearchSetting item: searchHistory) { savedHistory.add(item.writeToString()); } Main.pref.putCollection("search.history", savedHistory); } public static List<String> getSearchExpressionHistory() { ArrayList<String> ret = new ArrayList<String>(getSearchHistory().size()); for (SearchSetting ss: getSearchHistory()) { ret.add(ss.text); } return ret; } private static SearchSetting lastSearch = null; public SearchAction() { super(tr("Search..."), "dialogs/search", tr("Search for objects."), Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.GROUP_HOTKEY), true); putValue("help", ht("/Action/Search")); } public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; search(); } public void actionPerformed(ActionEvent e, Map<String, Object> parameters) { if (parameters.get(SEARCH_EXPRESSION) == null) { actionPerformed(e); } else { searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION)); } } public static SearchSetting showSearchDialog(SearchSetting initialValues) { if (initialValues == null) { initialValues = new SearchSetting(); } // -- prepare the combo box with the search expressions // JLabel label = new JLabel( initialValues instanceof Filter ? tr("Please enter a filter string.") : tr("Please enter a search string.")); final HistoryComboBox hcbSearchString = new HistoryComboBox(); hcbSearchString.setText(initialValues.text); hcbSearchString.getEditor().selectAll(); hcbSearchString.getEditor().getEditorComponent().requestFocusInWindow(); hcbSearchString.setToolTipText(tr("Enter the search expression")); // we have to reverse the history, because ComboBoxHistory will reverse it again // in addElement() // List<String> searchExpressionHistory = getSearchExpressionHistory(); Collections.reverse(searchExpressionHistory); hcbSearchString.setPossibleItems(searchExpressionHistory); JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace); JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add); JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove); JRadioButton in_selection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection); ButtonGroup bg = new ButtonGroup(); bg.add(replace); bg.add(add); bg.add(remove); bg.add(in_selection); JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive); JCheckBox regexSearch = new JCheckBox(tr("regular expression"), initialValues.regexSearch); JPanel left = new JPanel(new GridBagLayout()); left.add(label, GBC.eop()); left.add(hcbSearchString, GBC.eop().fill(GBC.HORIZONTAL)); left.add(replace, GBC.eol()); left.add(add, GBC.eol()); left.add(remove, GBC.eol()); left.add(in_selection, GBC.eop()); left.add(caseSensitive, GBC.eol()); left.add(regexSearch, GBC.eol()); JPanel right = new JPanel(); JLabel description = new JLabel("<html><ul>" + "<li>"+tr("<b>Baker Street</b> - ''Baker'' and ''Street'' in any key or name.")+"</li>" + "<li>"+tr("<b>\"Baker Street\"</b> - ''Baker Street'' in any key or name.")+"</li>" + "<li>"+tr("<b>name:Bak</b> - ''Bak'' anywhere in the name.")+"</li>" + "<li>"+tr("<b>type=route</b> - key ''type'' with value exactly ''route''.") + "</li>" + "<li>"+tr("<b>type=*</b> - key ''type'' with any value. Try also <b>*=value</b>, <b>type=</b>, <b>*=*</b>, <b>*=</b>") + "</li>" + "<li>"+tr("<b>-name:Bak</b> - not ''Bak'' in the name.")+"</li>" + "<li>"+tr("<b>oneway?</b> - oneway=yes, true, 1 or on")+"</li>" + "<li>"+tr("<b>foot:</b> - key=foot set to any value.")+"</li>" + "<li>"+tr("<u>Special targets:</u>")+"</li>" + "<li>"+tr("<b>type:</b> - type of the object (<b>node</b>, <b>way</b>, <b>relation</b>)")+"</li>" + "<li>"+tr("<b>user:</b>... - all objects changed by user")+"</li>" + "<li>"+tr("<b>user:anonymous</b> - all objects changed by anonymous users")+"</li>" + "<li>"+tr("<b>id:</b>... - object with given ID (0 for new objects)")+"</li>" + "<li>"+tr("<b>version:</b>... - object with given version (0 objects without an assigned version)")+"</li>" + "<li>"+tr("<b>changeset:</b>... - object with given changeset id (0 objects without assigned changeset)")+"</li>" + "<li>"+tr("<b>nodes:</b>... - object with given number of nodes (nodes:count or nodes:min-max)")+"</li>" + "<li>"+tr("<b>tags:</b>... - object with given number of tags (tags:count or tags:min-max)")+"</li>" + "<li>"+tr("<b>timestamp:</b>... - objects with this timestamp (<b>2009-11-12T14:51:09Z</b>, <b>2009-11-12</b> or <b>T14:51</b> ...)")+"</li>" + "<li>"+tr("<b>modified</b> - all changed objects")+"</li>" + "<li>"+tr("<b>selected</b> - all selected objects")+"</li>" + "<li>"+tr("<b>incomplete</b> - all incomplete objects")+"</li>" + "<li>"+tr("<b>untagged</b> - all untagged objects")+"</li>" + "<li>"+tr("<b>child <i>expr</i></b> - all children of objects matching the expression")+"</li>" + "<li>"+tr("<b>parent <i>expr</i></b> - all parents of objects matching the expression")+"</li>" + "<li>"+tr("Use <b>|</b> or <b>OR</b> to combine with logical or")+"</li>" + "<li>"+tr("Use <b>\"</b> to quote operators (e.g. if key contains <b>:</b>)") + "<br/>" + 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>).")+"</li>" + "<li>"+tr("Use <b>(</b> and <b>)</b> to group expressions")+"</li>" + "</ul></html>"); description.setFont(description.getFont().deriveFont(Font.PLAIN)); right.add(description); final JPanel p = new JPanel(); p.add(left); p.add(right); ExtendedDialog dialog = new ExtendedDialog( Main.parent, initialValues instanceof Filter ? tr("Filter") : tr("Search"), new String[] { initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"), tr("Cancel")} ); dialog.setButtonIcons(new String[] {"dialogs/search.png", "cancel.png"}); dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */); dialog.setContent(p); dialog.showDialog(); int result = dialog.getValue(); if(result != 1) return null; // User pressed OK - let's perform the search SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace : (add.isSelected() ? SearchAction.SearchMode.add : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection)); initialValues.text = hcbSearchString.getText(); initialValues.mode = mode; initialValues.caseSensitive = caseSensitive.isSelected(); initialValues.regexSearch = regexSearch.isSelected(); return initialValues; } /** * Launches the dialog for specifying search criteria and runs * a search */ public static void search() { SearchSetting se = showSearchDialog(lastSearch); if(se != null) { searchWithHistory(se); } } /** * Adds the search specified by the settings in <code>s</code> to the * search history and performs the search. * * @param s */ public static void searchWithHistory(SearchSetting s) { saveToHistory(s); lastSearch = new SearchSetting(s); search(s); } public static void searchWithoutHistory(SearchSetting s) { lastSearch = new SearchSetting(s); search(s); } public interface Function{ public Boolean isSomething(OsmPrimitive o); } public static int getSelection(SearchSetting s, Collection<OsmPrimitive> sel, Function f) { int foundMatches = 0; try { String searchText = s.text; if(s instanceof Filter){ searchText = "(" + s.text + ")" + (((Filter)s).applyForChildren ? ("| child (" + s.text + ")"): ""); searchText = (((Filter)s).inverted ? "-" : "") + "(" + searchText + ")"; } /*System.out.println(searchText);*/ SearchCompiler.Match matcher = SearchCompiler.compile(searchText, s.caseSensitive, s.regexSearch); if (s.mode == SearchMode.replace) { sel.clear(); } for (OsmPrimitive osm : Main.main.getCurrentDataSet().allNonDeletedCompletePrimitives()) { if (s.mode == SearchMode.replace) { if (matcher.match(osm)) { sel.add(osm); ++foundMatches; } } else if (s.mode == SearchMode.add && !f.isSomething(osm) && matcher.match(osm)) { sel.add(osm); ++foundMatches; } else if (s.mode == SearchMode.remove && f.isSomething(osm) && matcher.match(osm)) { sel.remove(osm); ++foundMatches; } else if (s.mode == SearchMode.in_selection && f.isSomething(osm)&& !matcher.match(osm)) { sel.remove(osm); ++foundMatches; } } } catch (SearchCompiler.ParseError e) { JOptionPane.showMessageDialog( Main.parent, e.getMessage(), tr("Error"), JOptionPane.ERROR_MESSAGE ); } return foundMatches; } public static void search(String search, SearchMode mode, boolean caseSensitive, boolean regexSearch) { search(new SearchSetting(search, mode, caseSensitive, regexSearch)); } public static void search(SearchSetting s) { // FIXME: This is confusing. The GUI says nothing about loading primitives from an URL. We'd like to *search* // for URLs in the current data set. // Disabling until a better solution is in place // // if (search.startsWith("http://") || search.startsWith("ftp://") || search.startsWith("https://") // || search.startsWith("file:/")) { // SelectionWebsiteLoader loader = new SelectionWebsiteLoader(search, mode); // if (loader.url != null && loader.url.getHost() != null) { // Main.worker.execute(loader); // return; // } // } final DataSet ds = Main.main.getCurrentDataSet(); Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(ds.getSelected()); int foundMatches = getSelection(s, sel, new Function(){ public Boolean isSomething(OsmPrimitive o){ return ds.isSelected(o); } }); ds.setSelected(sel); if (foundMatches == 0) { String msg = null; if (s.mode == SearchMode.replace) { msg = tr("No match found for ''{0}''", s.text); } else if (s.mode == SearchMode.add) { msg = tr("Nothing added to selection by searching for ''{0}''", s.text); } else if (s.mode == SearchMode.remove) { msg = tr("Nothing removed from selection by searching for ''{0}''", s.text); } else if (s.mode == SearchMode.in_selection) { msg = tr("Nothing found in selection by searching for ''{0}''", s.text); } Main.map.statusLine.setHelpText(msg); JOptionPane.showMessageDialog( Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE ); } else { Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); } } public static class SearchSetting { public String text; public SearchMode mode; public boolean caseSensitive; public boolean regexSearch; public SearchSetting() { this("", SearchMode.replace, false /* case insensitive */, false /* no regexp */); } public SearchSetting(String text, SearchMode mode, boolean caseSensitive, boolean regexSearch) { this.caseSensitive = caseSensitive; this.regexSearch = regexSearch; this.mode = mode; this.text = text; } public SearchSetting(SearchSetting original) { this(original.text, original.mode, original.caseSensitive, original.regexSearch); } @Override public String toString() { String cs = caseSensitive ? /*case sensitive*/ trc("search", "CS") : /*case insensitive*/ trc("search", "CI"); /*regex search*/ String rx = regexSearch ? (", " + trc("search", "RX")) : ""; return "\"" + text + "\" (" + cs + rx + ", " + mode + ")"; } @Override public boolean equals(Object other) { if(!(other instanceof SearchSetting)) return false; SearchSetting o = (SearchSetting) other; return (o.caseSensitive == this.caseSensitive && o.regexSearch == this.regexSearch && o.mode.equals(this.mode) && o.text.equals(this.text)); } @Override public int hashCode() { return text.hashCode(); } public static SearchSetting readFromString(String s) { if (s.length() == 0) return null; SearchSetting result = new SearchSetting(); int index = 1; result.mode = SearchMode.fromCode(s.charAt(0)); if (result.mode == null) { result.mode = SearchMode.replace; index = 0; } while (index < s.length()) { if (s.charAt(index) == 'C') { result.caseSensitive = true; } else if (s.charAt(index) == 'R') { result.regexSearch = true; } else if (s.charAt(index) == ' ') { break; } else { System.out.println("Uknown char in SearchSettings: " + s); break; } index++; } if (index < s.length() && s.charAt(index) == ' ') { index++; } result.text = s.substring(index); return result; } public String writeToString() { if (text == null || text.length() == 0) return ""; StringBuilder result = new StringBuilder(); result.append(mode.getCode()); if (caseSensitive) { result.append('C'); } if (regexSearch) { result.append('R'); } result.append(' '); result.append(text); return result.toString(); } } /** * Refreshes the enabled state * */ @Override protected void updateEnabledState() { setEnabled(getEditLayer() != null); } public List<ActionParameter<?>> getActionParameters() { return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); } }