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

Last change on this file since 4085 was 4085, checked in by bastiK, 13 years ago

applied #6308 (patch by Ole Jørgen Brønner) - Filter/search by closed-way area size

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