001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions.search; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Component; 009import java.awt.GraphicsEnvironment; 010import java.awt.event.ActionEvent; 011import java.awt.event.KeyEvent; 012import java.util.ArrayList; 013import java.util.Arrays; 014import java.util.Collection; 015import java.util.Collections; 016import java.util.HashSet; 017import java.util.LinkedHashSet; 018import java.util.LinkedList; 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022import java.util.function.Predicate; 023 024import javax.swing.JOptionPane; 025 026import org.openstreetmap.josm.actions.ActionParameter; 027import org.openstreetmap.josm.actions.ExpertToggleAction; 028import org.openstreetmap.josm.actions.JosmAction; 029import org.openstreetmap.josm.actions.ParameterizedAction; 030import org.openstreetmap.josm.data.osm.IPrimitive; 031import org.openstreetmap.josm.data.osm.OsmData; 032import org.openstreetmap.josm.data.osm.search.PushbackTokenizer; 033import org.openstreetmap.josm.data.osm.search.SearchCompiler; 034import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 035import org.openstreetmap.josm.data.osm.search.SearchCompiler.SimpleMatchFactory; 036import org.openstreetmap.josm.data.osm.search.SearchMode; 037import org.openstreetmap.josm.data.osm.search.SearchParseError; 038import org.openstreetmap.josm.data.osm.search.SearchSetting; 039import org.openstreetmap.josm.gui.MainApplication; 040import org.openstreetmap.josm.gui.MapFrame; 041import org.openstreetmap.josm.gui.Notification; 042import org.openstreetmap.josm.gui.PleaseWaitRunnable; 043import org.openstreetmap.josm.gui.dialogs.SearchDialog; 044import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 045import org.openstreetmap.josm.gui.preferences.ToolbarPreferences.ActionParser; 046import org.openstreetmap.josm.gui.progress.ProgressMonitor; 047import org.openstreetmap.josm.spi.preferences.Config; 048import org.openstreetmap.josm.tools.Logging; 049import org.openstreetmap.josm.tools.Shortcut; 050import org.openstreetmap.josm.tools.Utils; 051 052/** 053 * The search action allows the user to search the data layer using a complex search string. 054 * 055 * @see SearchCompiler 056 * @see SearchDialog 057 */ 058public class SearchAction extends JosmAction implements ParameterizedAction { 059 060 /** 061 * The default size of the search history 062 */ 063 public static final int DEFAULT_SEARCH_HISTORY_SIZE = 15; 064 /** 065 * Maximum number of characters before the search expression is shortened for display purposes. 066 */ 067 public static final int MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY = 100; 068 069 private static final String SEARCH_EXPRESSION = "searchExpression"; 070 071 private static final LinkedList<SearchSetting> searchHistory = new LinkedList<>(); 072 static { 073 SearchCompiler.addMatchFactory(new SimpleMatchFactory() { 074 @Override 075 public Collection<String> getKeywords() { 076 return Arrays.asList("inview", "allinview"); 077 } 078 079 @Override 080 public Match get(String keyword, boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) throws SearchParseError { 081 switch(keyword) { 082 case "inview": 083 return new InView(false); 084 case "allinview": 085 return new InView(true); 086 default: 087 throw new IllegalStateException("Not expecting keyword " + keyword); 088 } 089 } 090 }); 091 092 for (String s: Config.getPref().getList("search.history", Collections.<String>emptyList())) { 093 SearchSetting ss = SearchSetting.readFromString(s); 094 if (ss != null) { 095 searchHistory.add(ss); 096 } 097 } 098 } 099 100 /** 101 * Gets the search history 102 * @return The last searched terms. Do not modify it. 103 */ 104 public static Collection<SearchSetting> getSearchHistory() { 105 return searchHistory; 106 } 107 108 /** 109 * Saves a search to the search history. 110 * @param s The search to save 111 */ 112 public static void saveToHistory(SearchSetting s) { 113 if (searchHistory.isEmpty() || !s.equals(searchHistory.getFirst())) { 114 searchHistory.addFirst(new SearchSetting(s)); 115 } else if (searchHistory.contains(s)) { 116 // move existing entry to front, fixes #8032 - search history loses entries when re-using queries 117 searchHistory.remove(s); 118 searchHistory.addFirst(new SearchSetting(s)); 119 } 120 int maxsize = Config.getPref().getInt("search.history-size", DEFAULT_SEARCH_HISTORY_SIZE); 121 while (searchHistory.size() > maxsize) { 122 searchHistory.removeLast(); 123 } 124 Set<String> savedHistory = new LinkedHashSet<>(searchHistory.size()); 125 for (SearchSetting item: searchHistory) { 126 savedHistory.add(item.writeToString()); 127 } 128 Config.getPref().putList("search.history", new ArrayList<>(savedHistory)); 129 } 130 131 /** 132 * Gets a list of all texts that were recently used in the search 133 * @return The list of search texts. 134 */ 135 public static List<String> getSearchExpressionHistory() { 136 List<String> ret = new ArrayList<>(getSearchHistory().size()); 137 for (SearchSetting ss: getSearchHistory()) { 138 ret.add(ss.text); 139 } 140 return ret; 141 } 142 143 private static volatile SearchSetting lastSearch; 144 145 /** 146 * Constructs a new {@code SearchAction}. 147 */ 148 public SearchAction() { 149 super(tr("Search..."), "dialogs/search", tr("Search for objects"), 150 Shortcut.registerShortcut("system:find", tr("Search..."), KeyEvent.VK_F, Shortcut.CTRL), true); 151 setHelpId(ht("/Action/Search")); 152 } 153 154 @Override 155 public void actionPerformed(ActionEvent e) { 156 if (!isEnabled()) 157 return; 158 search(); 159 } 160 161 @Override 162 public void actionPerformed(ActionEvent e, Map<String, Object> parameters) { 163 if (parameters.get(SEARCH_EXPRESSION) == null) { 164 actionPerformed(e); 165 } else { 166 searchWithoutHistory((SearchSetting) parameters.get(SEARCH_EXPRESSION)); 167 } 168 } 169 170 171 /** 172 * Builds and shows the search dialog. 173 * @param initialValues A set of initial values needed in order to initialize the search dialog. 174 * If is {@code null}, then default settings are used. 175 * @return Returns new {@link SearchSetting} object containing parameters of the search. 176 */ 177 public static SearchSetting showSearchDialog(SearchSetting initialValues) { 178 if (initialValues == null) { 179 initialValues = new SearchSetting(); 180 } 181 182 SearchDialog dialog = new SearchDialog( 183 initialValues, getSearchExpressionHistory(), ExpertToggleAction.isExpert()); 184 185 if (dialog.showDialog().getValue() != 1) return null; 186 187 // User pressed OK - let's perform the search 188 SearchSetting searchSettings = dialog.getSearchSettings(); 189 190 if (dialog.isAddOnToolbar()) { 191 ToolbarPreferences.ActionDefinition aDef = 192 new ToolbarPreferences.ActionDefinition(MainApplication.getMenu().search); 193 aDef.getParameters().put(SEARCH_EXPRESSION, searchSettings); 194 // Display search expression as tooltip instead of generic one 195 aDef.setName(Utils.shortenString(searchSettings.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY)); 196 // parametrized action definition is now composed 197 ActionParser actionParser = new ToolbarPreferences.ActionParser(null); 198 String res = actionParser.saveAction(aDef); 199 200 // add custom search button to toolbar preferences 201 MainApplication.getToolbar().addCustomButton(res, -1, false); 202 } 203 204 return searchSettings; 205 } 206 207 /** 208 * Launches the dialog for specifying search criteria and runs a search 209 */ 210 public static void search() { 211 SearchSetting se = showSearchDialog(lastSearch); 212 if (se != null) { 213 searchWithHistory(se); 214 } 215 } 216 217 /** 218 * Adds the search specified by the settings in <code>s</code> to the 219 * search history and performs the search. 220 * 221 * @param s search settings 222 */ 223 public static void searchWithHistory(SearchSetting s) { 224 saveToHistory(s); 225 lastSearch = new SearchSetting(s); 226 search(s); 227 } 228 229 /** 230 * Performs the search specified by the settings in <code>s</code> without saving it to search history. 231 * 232 * @param s search settings 233 */ 234 public static void searchWithoutHistory(SearchSetting s) { 235 lastSearch = new SearchSetting(s); 236 search(s); 237 } 238 239 /** 240 * Performs the search specified by the search string {@code search} and the search mode {@code mode}. 241 * 242 * @param search the search string to use 243 * @param mode the search mode to use 244 */ 245 public static void search(String search, SearchMode mode) { 246 final SearchSetting searchSetting = new SearchSetting(); 247 searchSetting.text = search; 248 searchSetting.mode = mode; 249 search(searchSetting); 250 } 251 252 static void search(SearchSetting s) { 253 SearchTask.newSearchTask(s, new SelectSearchReceiver()).run(); 254 } 255 256 /** 257 * Performs the search specified by the search string {@code search} and the search mode {@code mode} and returns the result of the search. 258 * 259 * @param search the search string to use 260 * @param mode the search mode to use 261 * @return The result of the search. 262 * @since 10457 263 * @since 13950 (signature) 264 */ 265 public static Collection<IPrimitive> searchAndReturn(String search, SearchMode mode) { 266 final SearchSetting searchSetting = new SearchSetting(); 267 searchSetting.text = search; 268 searchSetting.mode = mode; 269 CapturingSearchReceiver receiver = new CapturingSearchReceiver(); 270 SearchTask.newSearchTask(searchSetting, receiver).run(); 271 return receiver.result; 272 } 273 274 /** 275 * Interfaces implementing this may receive the result of the current search. 276 * @author Michael Zangl 277 * @since 10457 278 * @since 10600 (functional interface) 279 * @since 13950 (signature) 280 */ 281 @FunctionalInterface 282 interface SearchReceiver { 283 /** 284 * Receive the search result 285 * @param ds The data set searched on. 286 * @param result The result collection, including the initial collection. 287 * @param foundMatches The number of matches added to the result. 288 * @param setting The setting used. 289 * @param parent parent component 290 */ 291 void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 292 int foundMatches, SearchSetting setting, Component parent); 293 } 294 295 /** 296 * Select the search result and display a status text for it. 297 */ 298 private static class SelectSearchReceiver implements SearchReceiver { 299 300 @Override 301 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, 302 int foundMatches, SearchSetting setting, Component parent) { 303 ds.setSelected(result); 304 MapFrame map = MainApplication.getMap(); 305 if (foundMatches == 0) { 306 final String msg; 307 final String text = Utils.shortenString(setting.text, MAX_LENGTH_SEARCH_EXPRESSION_DISPLAY); 308 if (setting.mode == SearchMode.replace) { 309 msg = tr("No match found for ''{0}''", text); 310 } else if (setting.mode == SearchMode.add) { 311 msg = tr("Nothing added to selection by searching for ''{0}''", text); 312 } else if (setting.mode == SearchMode.remove) { 313 msg = tr("Nothing removed from selection by searching for ''{0}''", text); 314 } else if (setting.mode == SearchMode.in_selection) { 315 msg = tr("Nothing found in selection by searching for ''{0}''", text); 316 } else { 317 msg = null; 318 } 319 if (map != null) { 320 map.statusLine.setHelpText(msg); 321 } 322 if (!GraphicsEnvironment.isHeadless()) { 323 new Notification(msg).show(); 324 } 325 } else { 326 map.statusLine.setHelpText(tr("Found {0} matches", foundMatches)); 327 } 328 } 329 } 330 331 /** 332 * This class stores the result of the search in a local variable. 333 * @author Michael Zangl 334 */ 335 private static final class CapturingSearchReceiver implements SearchReceiver { 336 private Collection<IPrimitive> result; 337 338 @Override 339 public void receiveSearchResult(OsmData<?, ?, ?, ?> ds, Collection<IPrimitive> result, int foundMatches, 340 SearchSetting setting, Component parent) { 341 this.result = result; 342 } 343 } 344 345 static final class SearchTask extends PleaseWaitRunnable { 346 private final OsmData<?, ?, ?, ?> ds; 347 private final SearchSetting setting; 348 private final Collection<IPrimitive> selection; 349 private final Predicate<IPrimitive> predicate; 350 private boolean canceled; 351 private int foundMatches; 352 private final SearchReceiver resultReceiver; 353 354 private SearchTask(OsmData<?, ?, ?, ?> ds, SearchSetting setting, Collection<IPrimitive> selection, 355 Predicate<IPrimitive> predicate, SearchReceiver resultReceiver) { 356 super(tr("Searching")); 357 this.ds = ds; 358 this.setting = setting; 359 this.selection = selection; 360 this.predicate = predicate; 361 this.resultReceiver = resultReceiver; 362 } 363 364 static SearchTask newSearchTask(SearchSetting setting, SearchReceiver resultReceiver) { 365 final OsmData<?, ?, ?, ?> ds = MainApplication.getLayerManager().getActiveData(); 366 if (ds == null) { 367 throw new IllegalStateException("No active dataset"); 368 } 369 return newSearchTask(setting, ds, resultReceiver); 370 } 371 372 /** 373 * Create a new search task for the given search setting. 374 * @param setting The setting to use 375 * @param ds The data set to search on 376 * @param resultReceiver will receive the search result 377 * @return A new search task. 378 */ 379 private static SearchTask newSearchTask(SearchSetting setting, final OsmData<?, ?, ?, ?> ds, SearchReceiver resultReceiver) { 380 final Collection<IPrimitive> selection = new HashSet<>(ds.getAllSelected()); 381 return new SearchTask(ds, setting, selection, IPrimitive::isSelected, resultReceiver); 382 } 383 384 @Override 385 protected void cancel() { 386 this.canceled = true; 387 } 388 389 @Override 390 protected void realRun() { 391 try { 392 foundMatches = 0; 393 SearchCompiler.Match matcher = SearchCompiler.compile(setting); 394 395 if (setting.mode == SearchMode.replace) { 396 selection.clear(); 397 } else if (setting.mode == SearchMode.in_selection) { 398 foundMatches = selection.size(); 399 } 400 401 Collection<? extends IPrimitive> all; 402 if (setting.allElements) { 403 all = ds.allPrimitives(); 404 } else { 405 all = ds.getPrimitives(p -> p.isSelectable()); // Do not use method reference before Java 11! 406 } 407 final ProgressMonitor subMonitor = getProgressMonitor().createSubTaskMonitor(all.size(), false); 408 subMonitor.beginTask(trn("Searching in {0} object", "Searching in {0} objects", all.size(), all.size())); 409 410 for (IPrimitive osm : all) { 411 if (canceled) { 412 return; 413 } 414 if (setting.mode == SearchMode.replace) { 415 if (matcher.match(osm)) { 416 selection.add(osm); 417 ++foundMatches; 418 } 419 } else if (setting.mode == SearchMode.add && !predicate.test(osm) && matcher.match(osm)) { 420 selection.add(osm); 421 ++foundMatches; 422 } else if (setting.mode == SearchMode.remove && predicate.test(osm) && matcher.match(osm)) { 423 selection.remove(osm); 424 ++foundMatches; 425 } else if (setting.mode == SearchMode.in_selection && predicate.test(osm) && !matcher.match(osm)) { 426 selection.remove(osm); 427 --foundMatches; 428 } 429 subMonitor.worked(1); 430 } 431 subMonitor.finishTask(); 432 } catch (SearchParseError e) { 433 Logging.debug(e); 434 JOptionPane.showMessageDialog( 435 MainApplication.getMainFrame(), 436 e.getMessage(), 437 tr("Error"), 438 JOptionPane.ERROR_MESSAGE 439 ); 440 } 441 } 442 443 @Override 444 protected void finish() { 445 if (canceled) { 446 return; 447 } 448 resultReceiver.receiveSearchResult(ds, selection, foundMatches, setting, getProgressMonitor().getWindowParent()); 449 } 450 } 451 452 /** 453 * {@link ActionParameter} implementation with {@link SearchSetting} as value type. 454 * @since 12547 (moved from {@link ActionParameter}) 455 */ 456 public static class SearchSettingsActionParameter extends ActionParameter<SearchSetting> { 457 458 /** 459 * Constructs a new {@code SearchSettingsActionParameter}. 460 * @param name parameter name (the key) 461 */ 462 public SearchSettingsActionParameter(String name) { 463 super(name); 464 } 465 466 @Override 467 public Class<SearchSetting> getType() { 468 return SearchSetting.class; 469 } 470 471 @Override 472 public SearchSetting readFromString(String s) { 473 return SearchSetting.readFromString(s); 474 } 475 476 @Override 477 public String writeToString(SearchSetting value) { 478 if (value == null) 479 return ""; 480 return value.writeToString(); 481 } 482 } 483 484 /** 485 * Refreshes the enabled state 486 */ 487 @Override 488 protected void updateEnabledState() { 489 setEnabled(getLayerManager().getActiveData() != null); 490 } 491 492 @Override 493 public List<ActionParameter<?>> getActionParameters() { 494 return Collections.<ActionParameter<?>>singletonList(new SearchSettingsActionParameter(SEARCH_EXPRESSION)); 495 } 496}