source: josm/trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java@ 3355

Last change on this file since 3355 was 3355, checked in by jttt, 14 years ago

Simplify/optimize filters implementation

  • Property svn:eol-style set to native
File size: 22.8 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions.search;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trc;
7
8import java.awt.Font;
9import java.awt.GridBagLayout;
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.HashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Map;
19
20import javax.swing.ButtonGroup;
21import javax.swing.JCheckBox;
22import javax.swing.JLabel;
23import javax.swing.JOptionPane;
24import javax.swing.JPanel;
25import javax.swing.JRadioButton;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.actions.ActionParameter;
29import org.openstreetmap.josm.actions.JosmAction;
30import org.openstreetmap.josm.actions.ParameterizedAction;
31import org.openstreetmap.josm.actions.ActionParameter.SearchSettingsActionParameter;
32import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.osm.Filter;
35import org.openstreetmap.josm.data.osm.OsmPrimitive;
36import org.openstreetmap.josm.gui.ExtendedDialog;
37import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
38import org.openstreetmap.josm.tools.GBC;
39import org.openstreetmap.josm.tools.Property;
40import org.openstreetmap.josm.tools.Shortcut;
41
42public class SearchAction extends JosmAction implements ParameterizedAction {
43
44 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 10;
45
46 private static final String SEARCH_EXPRESSION = "searchExpression";
47
48 public static enum SearchMode {
49 replace('R'), add('A'), remove('D'), in_selection('S');
50
51 private final char code;
52
53 SearchMode(char code) {
54 this.code = code;
55 }
56
57 public char getCode() {
58 return code;
59 }
60
61 public static SearchMode fromCode(char code) {
62 for (SearchMode mode: values()) {
63 if (mode.getCode() == code)
64 return mode;
65 }
66 return null;
67 }
68 }
69
70 private static LinkedList<SearchSetting> searchHistory = null;
71
72 public static Collection<SearchSetting> getSearchHistory() {
73 if (searchHistory == null) {
74 searchHistory = new LinkedList<SearchSetting>();
75 for (String s: Main.pref.getCollection("search.history", Collections.<String>emptyList())) {
76 SearchSetting ss = SearchSetting.readFromString(s);
77 if (ss != null) {
78 searchHistory.add(ss);
79 }
80 }
81 }
82
83 return searchHistory;
84 }
85
86 public static void saveToHistory(SearchSetting s) {
87 if(searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) {
88 searchHistory.addFirst(new SearchSetting(s));
89 }
90 int maxsize = Main.pref.getInteger("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE);
91 while (searchHistory.size() > maxsize) {
92 searchHistory.removeLast();
93 }
94 List<String> savedHistory = new ArrayList<String>();
95 for (SearchSetting item: searchHistory) {
96 savedHistory.add(item.writeToString());
97 }
98 Main.pref.putCollection("search.history", savedHistory);
99 }
100
101 public static List<String> getSearchExpressionHistory() {
102 ArrayList<String> ret = new ArrayList<String>(getSearchHistory().size());
103 for (SearchSetting ss: getSearchHistory()) {
104 ret.add(ss.text);
105 }
106 return ret;
107 }
108
109 private static SearchSetting lastSearch = null;
110
111 public SearchAction() {
112 super(tr("Search..."), "dialogs/search", tr("Search for objects."),
113 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.GROUP_HOTKEY), true);
114 putValue("help", ht("/Action/Search"));
115 }
116
117 public void actionPerformed(ActionEvent e) {
118 if (!isEnabled())
119 return;
120 search();
121 }
122
123 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) {
124 if (parameters.get(SEARCH_EXPRESSION) == null) {
125 actionPerformed(e);
126 } else {
127 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION));
128 }
129 }
130
131 public static SearchSetting showSearchDialog(SearchSetting initialValues) {
132 if (initialValues == null) {
133 initialValues = new SearchSetting();
134 }
135 // -- prepare the combo box with the search expressions
136 //
137 JLabel label = new JLabel( initialValues instanceof Filter ? tr("Please enter a filter string.") : tr("Please enter a search string."));
138 final HistoryComboBox hcbSearchString = new HistoryComboBox();
139 hcbSearchString.setText(initialValues.text);
140 hcbSearchString.getEditor().selectAll();
141 hcbSearchString.getEditor().getEditorComponent().requestFocusInWindow();
142 hcbSearchString.setToolTipText(tr("Enter the search expression"));
143 // we have to reverse the history, because ComboBoxHistory will reverse it again
144 // in addElement()
145 //
146 List<String> searchExpressionHistory = getSearchExpressionHistory();
147 Collections.reverse(searchExpressionHistory);
148 hcbSearchString.setPossibleItems(searchExpressionHistory);
149
150 JRadioButton replace = new JRadioButton(tr("replace selection"), initialValues.mode == SearchMode.replace);
151 JRadioButton add = new JRadioButton(tr("add to selection"), initialValues.mode == SearchMode.add);
152 JRadioButton remove = new JRadioButton(tr("remove from selection"), initialValues.mode == SearchMode.remove);
153 JRadioButton in_selection = new JRadioButton(tr("find in selection"), initialValues.mode == SearchMode.in_selection);
154 ButtonGroup bg = new ButtonGroup();
155 bg.add(replace);
156 bg.add(add);
157 bg.add(remove);
158 bg.add(in_selection);
159
160 final JCheckBox caseSensitive = new JCheckBox(tr("case sensitive"), initialValues.caseSensitive);
161 JCheckBox allElements = new JCheckBox(tr("all objects"), initialValues.allElements);
162 allElements.setToolTipText(tr("Also include incomplete and deleted objects in search."));
163 final JCheckBox regexSearch = new JCheckBox(tr("regular expression"), initialValues.regexSearch);
164
165 JPanel left = new JPanel(new GridBagLayout());
166 left.add(label, GBC.eop());
167 left.add(hcbSearchString, GBC.eop().fill(GBC.HORIZONTAL));
168 left.add(replace, GBC.eol());
169 left.add(add, GBC.eol());
170 left.add(remove, GBC.eol());
171 left.add(in_selection, GBC.eop());
172 left.add(caseSensitive, GBC.eol());
173 left.add(allElements, GBC.eol());
174 left.add(regexSearch, GBC.eol());
175
176 JPanel right = new JPanel();
177 JLabel description =
178 new JLabel("<html><ul>"
179 + "<li>"+tr("<b>Baker Street</b> - ''Baker'' and ''Street'' in any key or name.")+"</li>"
180 + "<li>"+tr("<b>\"Baker Street\"</b> - ''Baker Street'' in any key or name.")+"</li>"
181 + "<li>"+tr("<b>name:Bak</b> - ''Bak'' anywhere in the name.")+"</li>"
182 + "<li>"+tr("<b>type=route</b> - key ''type'' with value exactly ''route''.") + "</li>"
183 + "<li>"+tr("<b>type=*</b> - key ''type'' with any value. Try also <b>*=value</b>, <b>type=</b>, <b>*=*</b>, <b>*=</b>") + "</li>"
184 + "<li>"+tr("<b>-name:Bak</b> - not ''Bak'' in the name.")+"</li>"
185 + "<li>"+tr("<b>oneway?</b> - oneway=yes, true, 1 or on")+"</li>"
186 + "<li>"+tr("<b>foot:</b> - key=foot set to any value.")+"</li>"
187 + "<li>"+tr("<u>Special targets:</u>")+"</li>"
188 + "<li>"+tr("<b>type:</b> - type of the object (<b>node</b>, <b>way</b>, <b>relation</b>)")+"</li>"
189 + "<li>"+tr("<b>user:</b>... - all objects changed by user")+"</li>"
190 + "<li>"+tr("<b>user:anonymous</b> - all objects changed by anonymous users")+"</li>"
191 + "<li>"+tr("<b>id:</b>... - object with given ID (0 for new objects)")+"</li>"
192 + "<li>"+tr("<b>version:</b>... - object with given version (0 objects without an assigned version)")+"</li>"
193 + "<li>"+tr("<b>changeset:</b>... - object with given changeset id (0 objects without assigned changeset)")+"</li>"
194 + "<li>"+tr("<b>nodes:</b>... - object with given number of nodes (nodes:count or nodes:min-max)")+"</li>"
195 + "<li>"+tr("<b>tags:</b>... - object with given number of tags (tags:count or tags:min-max)")+"</li>"
196 + "<li>"+tr("<b>role:</b>... - object with given role in a relation")+"</li>"
197 + "<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>"
198 + "<li>"+tr("<b>modified</b> - all changed objects")+"</li>"
199 + "<li>"+tr("<b>selected</b> - all selected objects")+"</li>"
200 + "<li>"+tr("<b>incomplete</b> - all incomplete objects")+"</li>"
201 + "<li>"+tr("<b>untagged</b> - all untagged objects")+"</li>"
202 + "<li>"+tr("<b>child <i>expr</i></b> - all children of objects matching the expression")+"</li>"
203 + "<li>"+tr("<b>parent <i>expr</i></b> - all parents of objects matching the expression")+"</li>"
204 + "<li>"+tr("Use <b>|</b> or <b>OR</b> to combine with logical or")+"</li>"
205 + "<li>"+tr("Use <b>\"</b> to quote operators (e.g. if key contains <b>:</b>)") + "<br/>"
206 + 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>"
207 + "<li>"+tr("Use <b>(</b> and <b>)</b> to group expressions")+"</li>"
208 + "</ul></html>");
209 description.setFont(description.getFont().deriveFont(Font.PLAIN));
210 right.add(description);
211
212 final JPanel p = new JPanel();
213 p.add(left);
214 p.add(right);
215 ExtendedDialog dialog = new ExtendedDialog(
216 Main.parent,
217 initialValues instanceof Filter ? tr("Filter") : tr("Search"),
218 new String[] {
219 initialValues instanceof Filter ? tr("Submit filter") : tr("Start Search"),
220 tr("Cancel")}
221 ) {
222 @Override
223 protected void buttonAction(int buttonIndex, ActionEvent evt) {
224 if (buttonIndex == 0) {
225 try {
226 SearchCompiler.compile(hcbSearchString.getText(), caseSensitive.isSelected(), regexSearch.isSelected());
227 super.buttonAction(buttonIndex, evt);
228 } catch (ParseError e) {
229 JOptionPane.showMessageDialog(
230 Main.parent,
231 tr("Search expression is not valid: \n\n {0}", e.getMessage()),
232 tr("Invalid search expression"),
233 JOptionPane.ERROR_MESSAGE);
234 }
235 } else {
236 super.buttonAction(buttonIndex, evt);
237 }
238 }
239 };
240 dialog.setButtonIcons(new String[] {"dialogs/search.png", "cancel.png"});
241 dialog.configureContextsensitiveHelp("/Action/Search", true /* show help button */);
242 dialog.setContent(p);
243 dialog.showDialog();
244 int result = dialog.getValue();
245
246 if(result != 1) return null;
247
248 // User pressed OK - let's perform the search
249 SearchMode mode = replace.isSelected() ? SearchAction.SearchMode.replace
250 : (add.isSelected() ? SearchAction.SearchMode.add
251 : (remove.isSelected() ? SearchAction.SearchMode.remove : SearchAction.SearchMode.in_selection));
252 initialValues.text = hcbSearchString.getText();
253 initialValues.mode = mode;
254 initialValues.caseSensitive = caseSensitive.isSelected();
255 initialValues.allElements = allElements.isSelected();
256 initialValues.regexSearch = regexSearch.isSelected();
257 return initialValues;
258 }
259
260 /**
261 * Launches the dialog for specifying search criteria and runs
262 * a search
263 */
264 public static void search() {
265 SearchSetting se = showSearchDialog(lastSearch);
266 if(se != null) {
267 searchWithHistory(se);
268 }
269 }
270
271 /**
272 * Adds the search specified by the settings in <code>s</code> to the
273 * search history and performs the search.
274 *
275 * @param s
276 */
277 public static void searchWithHistory(SearchSetting s) {
278 saveToHistory(s);
279 lastSearch = new SearchSetting(s);
280 search(s);
281 }
282
283 public static void searchWithoutHistory(SearchSetting s) {
284 lastSearch = new SearchSetting(s);
285 search(s);
286 }
287
288 public interface Function{
289 public Boolean isSomething(OsmPrimitive o);
290 }
291
292 public static int getSelection(SearchSetting s, Collection<OsmPrimitive> sel, Function f) {
293 int foundMatches = 0;
294 try {
295 String searchText = s.text;
296 SearchCompiler.Match matcher = SearchCompiler.compile(searchText, s.caseSensitive, s.regexSearch);
297
298 if (s.mode == SearchMode.replace) {
299 sel.clear();
300 }
301
302 Collection<OsmPrimitive> all;
303 if(s.allElements) {
304 all = Main.main.getCurrentDataSet().allPrimitives();
305 } else {
306 all = Main.main.getCurrentDataSet().allNonDeletedCompletePrimitives();
307 }
308 for (OsmPrimitive osm : all) {
309 if (s.mode == SearchMode.replace) {
310 if (matcher.match(osm)) {
311 sel.add(osm);
312 ++foundMatches;
313 }
314 } else if (s.mode == SearchMode.add && !f.isSomething(osm) && matcher.match(osm)) {
315 sel.add(osm);
316 ++foundMatches;
317 } else if (s.mode == SearchMode.remove && f.isSomething(osm) && matcher.match(osm)) {
318 sel.remove(osm);
319 ++foundMatches;
320 } else if (s.mode == SearchMode.in_selection && f.isSomething(osm)&& !matcher.match(osm)) {
321 sel.remove(osm);
322 ++foundMatches;
323 }
324 }
325 } catch (SearchCompiler.ParseError e) {
326 JOptionPane.showMessageDialog(
327 Main.parent,
328 e.getMessage(),
329 tr("Error"),
330 JOptionPane.ERROR_MESSAGE
331
332 );
333 }
334 return foundMatches;
335 }
336
337 /**
338 * Version of getSelection that is customized for filter, but should
339 * also work in other context.
340 *
341 * @param s the search settings
342 * @param all the collection of all the primitives that should be considered
343 * @param p the property that should be set/unset if something is found
344 */
345 public static void getSelection(SearchSetting s, Collection<OsmPrimitive> all, Property<OsmPrimitive, Boolean> p) {
346 try {
347 String searchText = s.text;
348 if (s instanceof Filter && ((Filter)s).inverted) {
349 searchText = String.format("-(%s)", searchText);
350 }
351 SearchCompiler.Match matcher = SearchCompiler.compile(searchText, s.caseSensitive, s.regexSearch);
352
353 for (OsmPrimitive osm : all) {
354 if (s.mode == SearchMode.replace) {
355 if (matcher.match(osm)) {
356 p.set(osm, true);
357 } else {
358 p.set(osm, false);
359 }
360 } else if (s.mode == SearchMode.add && !p.get(osm) && matcher.match(osm)) {
361 p.set(osm, true);
362 } else if (s.mode == SearchMode.remove && p.get(osm) && matcher.match(osm)) {
363 p.set(osm, false);
364 } else if (s.mode == SearchMode.in_selection && p.get(osm) && !matcher.match(osm)) {
365 p.set(osm, false);
366 }
367 }
368 } catch (SearchCompiler.ParseError e) {
369 JOptionPane.showMessageDialog(
370 Main.parent,
371 e.getMessage(),
372 tr("Error"),
373 JOptionPane.ERROR_MESSAGE
374
375 );
376 }
377 }
378
379 public static void search(String search, SearchMode mode) {
380 search(new SearchSetting(search, mode, false, false, false));
381 }
382
383 public static void search(SearchSetting s) {
384 // FIXME: This is confusing. The GUI says nothing about loading primitives from an URL. We'd like to *search*
385 // for URLs in the current data set.
386 // Disabling until a better solution is in place
387 //
388 // if (search.startsWith("http://") || search.startsWith("ftp://") || search.startsWith("https://")
389 // || search.startsWith("file:/")) {
390 // SelectionWebsiteLoader loader = new SelectionWebsiteLoader(search, mode);
391 // if (loader.url != null && loader.url.getHost() != null) {
392 // Main.worker.execute(loader);
393 // return;
394 // }
395 // }
396
397 final DataSet ds = Main.main.getCurrentDataSet();
398 Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(ds.getSelected());
399 int foundMatches = getSelection(s, sel, new Function(){
400 public Boolean isSomething(OsmPrimitive o){
401 return ds.isSelected(o);
402 }
403 });
404 ds.setSelected(sel);
405 if (foundMatches == 0) {
406 String msg = null;
407 if (s.mode == SearchMode.replace) {
408 msg = tr("No match found for ''{0}''", s.text);
409 } else if (s.mode == SearchMode.add) {
410 msg = tr("Nothing added to selection by searching for ''{0}''", s.text);
411 } else if (s.mode == SearchMode.remove) {
412 msg = tr("Nothing removed from selection by searching for ''{0}''", s.text);
413 } else if (s.mode == SearchMode.in_selection) {
414 msg = tr("Nothing found in selection by searching for ''{0}''", s.text);
415 }
416 Main.map.statusLine.setHelpText(msg);
417 JOptionPane.showMessageDialog(
418 Main.parent,
419 msg,
420 tr("Warning"),
421 JOptionPane.WARNING_MESSAGE
422 );
423 } else {
424 Main.map.statusLine.setHelpText(tr("Found {0} matches", foundMatches));
425 }
426 }
427
428 public static class SearchSetting {
429 public String text;
430 public SearchMode mode;
431 public boolean caseSensitive;
432 public boolean regexSearch;
433 public boolean allElements;
434
435 public SearchSetting() {
436 this("", SearchMode.replace, false /* case insensitive */,
437 false /* no regexp */, false /* only useful primitives */);
438 }
439
440 public SearchSetting(String text, SearchMode mode, boolean caseSensitive,
441 boolean regexSearch, boolean allElements) {
442 this.caseSensitive = caseSensitive;
443 this.regexSearch = regexSearch;
444 this.allElements = allElements;
445 this.mode = mode;
446 this.text = text;
447 }
448
449 public SearchSetting(SearchSetting original) {
450 this(original.text, original.mode, original.caseSensitive,
451 original.regexSearch, original.allElements);
452 }
453
454 @Override
455 public String toString() {
456 String cs = caseSensitive ?
457 /*case sensitive*/ trc("search", "CS") :
458 /*case insensitive*/ trc("search", "CI");
459 String rx = regexSearch ? (", " +
460 /*regex search*/ trc("search", "RX")) : "";
461 String all = allElements ? (", " +
462 /*all elements*/ trc("search", "A")) : "";
463 return "\"" + text + "\" (" + cs + rx + all + ", " + mode + ")";
464 }
465
466 @Override
467 public boolean equals(Object other) {
468 if(!(other instanceof SearchSetting))
469 return false;
470 SearchSetting o = (SearchSetting) other;
471 return (o.caseSensitive == this.caseSensitive
472 && o.regexSearch == this.regexSearch
473 && o.allElements == this.allElements
474 && o.mode.equals(this.mode)
475 && o.text.equals(this.text));
476 }
477
478 @Override
479 public int hashCode() {
480 return text.hashCode();
481 }
482
483 public static SearchSetting readFromString(String s) {
484 if (s.length() == 0)
485 return null;
486
487 SearchSetting result = new SearchSetting();
488
489 int index = 1;
490
491 result.mode = SearchMode.fromCode(s.charAt(0));
492 if (result.mode == null) {
493 result.mode = SearchMode.replace;
494 index = 0;
495 }
496
497 while (index < s.length()) {
498 if (s.charAt(index) == 'C') {
499 result.caseSensitive = true;
500 } else if (s.charAt(index) == 'R') {
501 result.regexSearch = true;
502 } else if (s.charAt(index) == 'A') {
503 result.allElements = true;
504 } else if (s.charAt(index) == ' ') {
505 break;
506 } else {
507 System.out.println("Uknown char in SearchSettings: " + s);
508 break;
509 }
510 index++;
511 }
512
513 if (index < s.length() && s.charAt(index) == ' ') {
514 index++;
515 }
516
517 result.text = s.substring(index);
518
519 return result;
520 }
521
522 public String writeToString() {
523 if (text == null || text.length() == 0)
524 return "";
525
526 StringBuilder result = new StringBuilder();
527 result.append(mode.getCode());
528 if (caseSensitive) {
529 result.append('C');
530 }
531 if (regexSearch) {
532 result.append('R');
533 }
534 if (allElements) {
535 result.append('A');
536 }
537 result.append(' ');
538 result.append(text);
539 return result.toString();
540 }
541 }
542
543 /**
544 * Refreshes the enabled state
545 *
546 */
547 @Override
548 protected void updateEnabledState() {
549 setEnabled(getEditLayer() != null);
550 }
551
552 public List<ActionParameter<?>> getActionParameters() {
553 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION));
554 }
555}
Note: See TracBrowser for help on using the repository browser.