001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.awt.Graphics2D; 008import java.util.ArrayList; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.HashSet; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Set; 015import java.util.Stack; 016 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.data.SortableModel; 020import org.openstreetmap.josm.data.StructUtils; 021import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry; 022import org.openstreetmap.josm.data.osm.search.SearchParseError; 023import org.openstreetmap.josm.gui.MainApplication; 024import org.openstreetmap.josm.gui.widgets.OSDLabel; 025import org.openstreetmap.josm.spi.preferences.Config; 026import org.openstreetmap.josm.tools.Logging; 027import org.openstreetmap.josm.tools.Utils; 028 029/** 030 * The model that is used both for auto and manual filters. 031 * @since 12400 032 */ 033public class FilterModel implements SortableModel<Filter> { 034 035 /** 036 * number of primitives that are disabled but not hidden 037 */ 038 private int disabledCount; 039 /** 040 * number of primitives that are disabled and hidden 041 */ 042 private int disabledAndHiddenCount; 043 /** 044 * true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process 045 */ 046 private boolean changed; 047 048 private final List<Filter> filters = new LinkedList<>(); 049 private final FilterMatcher filterMatcher = new FilterMatcher(); 050 051 private void updateFilterMatcher() { 052 filterMatcher.reset(); 053 for (Filter filter : filters) { 054 try { 055 filterMatcher.add(filter); 056 } catch (SearchParseError e) { 057 Logging.error(e); 058 JOptionPane.showMessageDialog( 059 MainApplication.getMainFrame(), 060 tr("<html>Error in filter <code>{0}</code>:<br>{1}", 061 Utils.escapeReservedCharactersHTML(Utils.shortenString(filter.text, 80)), 062 Utils.escapeReservedCharactersHTML(e.getMessage())), 063 tr("Error in filter"), 064 JOptionPane.ERROR_MESSAGE); 065 filter.enable = false; 066 } 067 } 068 } 069 070 /** 071 * Initializes the model from preferences. 072 * @param prefEntry preference key 073 */ 074 public void loadPrefs(String prefEntry) { 075 List<FilterPreferenceEntry> entries = StructUtils.getListOfStructs( 076 Config.getPref(), prefEntry, null, FilterPreferenceEntry.class); 077 if (entries != null) { 078 for (FilterPreferenceEntry e : entries) { 079 filters.add(new Filter(e)); 080 } 081 updateFilterMatcher(); 082 } 083 } 084 085 /** 086 * Saves the model to preferences. 087 * @param prefEntry preferences key 088 */ 089 public void savePrefs(String prefEntry) { 090 Collection<FilterPreferenceEntry> entries = new ArrayList<>(); 091 for (Filter flt : filters) { 092 entries.add(flt.getPreferenceEntry()); 093 } 094 StructUtils.putListOfStructs(Config.getPref(), prefEntry, entries, FilterPreferenceEntry.class); 095 } 096 097 /** 098 * Runs the filters on the current edit data set. 099 */ 100 public void executeFilters() { 101 DataSet ds = OsmDataManager.getInstance().getActiveDataSet(); 102 changed = false; 103 if (ds == null) { 104 disabledAndHiddenCount = 0; 105 disabledCount = 0; 106 changed = true; 107 } else { 108 final Collection<OsmPrimitive> deselect = new HashSet<>(); 109 110 ds.beginUpdate(); 111 try { 112 113 final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives(); 114 115 changed = FilterWorker.executeFilters(all, filterMatcher); 116 117 disabledCount = 0; 118 disabledAndHiddenCount = 0; 119 // collect disabled and selected the primitives 120 for (OsmPrimitive osm : all) { 121 if (osm.isDisabled()) { 122 disabledCount++; 123 if (osm.isSelected()) { 124 deselect.add(osm); 125 } 126 if (osm.isDisabledAndHidden()) { 127 disabledAndHiddenCount++; 128 } 129 } 130 } 131 disabledCount -= disabledAndHiddenCount; 132 } finally { 133 if (changed) { 134 ds.fireFilterChanged(); 135 } 136 ds.endUpdate(); 137 } 138 139 if (!deselect.isEmpty()) { 140 ds.clearSelection(deselect); 141 } 142 } 143 if (changed) { 144 updateMap(); 145 } 146 } 147 148 /** 149 * Runs the filter on a list of primitives that are part of the edit data set. 150 * @param primitives The primitives 151 */ 152 public void executeFilters(Collection<? extends OsmPrimitive> primitives) { 153 DataSet ds = OsmDataManager.getInstance().getActiveDataSet(); 154 if (ds == null) 155 return; 156 157 changed = false; 158 List<OsmPrimitive> deselect = new ArrayList<>(); 159 160 ds.beginUpdate(); 161 try { 162 for (int i = 0; i < 2; i++) { 163 for (OsmPrimitive primitive: primitives) { 164 165 if (i == 0 && primitive instanceof Node) { 166 continue; 167 } 168 169 if (i == 1 && !(primitive instanceof Node)) { 170 continue; 171 } 172 173 if (primitive.isDisabled()) { 174 disabledCount--; 175 } 176 if (primitive.isDisabledAndHidden()) { 177 disabledAndHiddenCount--; 178 } 179 changed |= FilterWorker.executeFilters(primitive, filterMatcher); 180 if (primitive.isDisabled()) { 181 disabledCount++; 182 } 183 if (primitive.isDisabledAndHidden()) { 184 disabledAndHiddenCount++; 185 } 186 187 if (primitive.isSelected() && primitive.isDisabled()) { 188 deselect.add(primitive); 189 } 190 } 191 } 192 } finally { 193 ds.endUpdate(); 194 } 195 196 if (!deselect.isEmpty()) { 197 ds.clearSelection(deselect); 198 } 199 if (changed) { 200 updateMap(); 201 } 202 } 203 204 private static void updateMap() { 205 MainApplication.getLayerManager().invalidateEditLayer(); 206 } 207 208 /** 209 * Clears all filtered flags from all primitives in the dataset 210 */ 211 public void clearFilterFlags() { 212 DataSet ds = OsmDataManager.getInstance().getActiveDataSet(); 213 if (ds != null) { 214 FilterWorker.clearFilterFlags(ds.allPrimitives()); 215 } 216 disabledCount = 0; 217 disabledAndHiddenCount = 0; 218 } 219 220 /** 221 * Removes all filters from this model. 222 */ 223 public void clearFilters() { 224 filters.clear(); 225 updateFilterMatcher(); 226 } 227 228 /** 229 * Adds a new filter to the filter list. 230 * @param filter The new filter 231 * @return true (as specified by {@link Collection#add}) 232 */ 233 public boolean addFilter(Filter filter) { 234 filters.add(filter); 235 updateFilterMatcher(); 236 return true; 237 } 238 239 /** 240 * Moves the filters in the given rows by a number of positions. 241 * @param delta negative or positive increment 242 * @param rowIndexes The filter rows 243 * @return true if the filters have been moved down 244 * @since 15226 245 */ 246 public boolean moveFilters(int delta, int... rowIndexes) { 247 if (!canMove(delta, filters::size, rowIndexes)) 248 return false; 249 doMove(delta, rowIndexes); 250 updateFilterMatcher(); 251 return true; 252 } 253 254 /** 255 * Moves down the filter in the given row. 256 * @param rowIndex The filter row 257 * @return true if the filter has been moved down 258 */ 259 public boolean moveDownFilter(int rowIndex) { 260 return moveFilters(1, rowIndex); 261 } 262 263 /** 264 * Moves up the filter in the given row 265 * @param rowIndex The filter row 266 * @return true if the filter has been moved up 267 */ 268 public boolean moveUpFilter(int rowIndex) { 269 return moveFilters(-1, rowIndex); 270 } 271 272 /** 273 * Removes the filter that is displayed in the given row 274 * @param rowIndex The index of the filter to remove 275 * @return the filter previously at the specified position 276 */ 277 public Filter removeFilter(int rowIndex) { 278 Filter result = filters.remove(rowIndex); 279 updateFilterMatcher(); 280 return result; 281 } 282 283 /** 284 * Sets/replaces the filter for a given row. 285 * @param rowIndex The row index 286 * @param filter The filter that should be placed in that row 287 * @return the filter previously at the specified position 288 * @deprecated Use {@link #setValue} 289 */ 290 @Deprecated 291 public Filter setFilter(int rowIndex, Filter filter) { 292 return setValue(rowIndex, filter); 293 } 294 295 @Override 296 public Filter setValue(int rowIndex, Filter filter) { 297 Filter result = filters.set(rowIndex, filter); 298 updateFilterMatcher(); 299 return result; 300 } 301 302 /** 303 * Gets the filter by row index 304 * @param rowIndex The row index 305 * @return The filter in that row 306 * @deprecated Use {@link #getValue} 307 */ 308 @Deprecated 309 public Filter getFilter(int rowIndex) { 310 return getValue(rowIndex); 311 } 312 313 @Override 314 public Filter getValue(int rowIndex) { 315 return filters.get(rowIndex); 316 } 317 318 /** 319 * Draws a text on the map display that indicates that filters are active. 320 * @param g The graphics to draw that text on. 321 * @param lblOSD On Screen Display label 322 * @param header The title to display at the beginning of OSD 323 * @param footer The message to display at the bottom of OSD. Must end by {@code </html>} 324 */ 325 public void drawOSDText(Graphics2D g, OSDLabel lblOSD, String header, String footer) { 326 if (disabledCount == 0 && disabledAndHiddenCount == 0) 327 return; 328 329 String message = "<html>" + header; 330 331 if (disabledAndHiddenCount != 0) { 332 /* for correct i18n of plural forms - see #9110 */ 333 message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount); 334 } 335 336 if (disabledAndHiddenCount != 0 && disabledCount != 0) { 337 message += "<br>"; 338 } 339 340 if (disabledCount != 0) { 341 /* for correct i18n of plural forms - see #9110 */ 342 message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount); 343 } 344 345 message += footer; 346 347 lblOSD.setText(message); 348 lblOSD.setSize(lblOSD.getPreferredSize()); 349 350 int dx = MainApplication.getMap().mapView.getWidth() - lblOSD.getPreferredSize().width - 15; 351 int dy = 15; 352 g.translate(dx, dy); 353 lblOSD.paintComponent(g); 354 g.translate(-dx, -dy); 355 } 356 357 /** 358 * Returns the list of filters. 359 * @return the list of filters 360 */ 361 public List<Filter> getFilters() { 362 return new ArrayList<>(filters); 363 } 364 365 /** 366 * Returns the number of filters. 367 * @return the number of filters 368 */ 369 public int getFiltersCount() { 370 return filters.size(); 371 } 372 373 /** 374 * Returns the number of primitives that are disabled but not hidden. 375 * @return the number of primitives that are disabled but not hidden 376 */ 377 public int getDisabledCount() { 378 return disabledCount; 379 } 380 381 /** 382 * Returns the number of primitives that are disabled and hidden. 383 * @return the number of primitives that are disabled and hidden 384 */ 385 public int getDisabledAndHiddenCount() { 386 return disabledAndHiddenCount; 387 } 388 389 /** 390 * Determines if the filter state (normal / disabled / hidden) of any primitive has changed in the process. 391 * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process 392 */ 393 public boolean isChanged() { 394 return changed; 395 } 396 397 /** 398 * Determines if at least one filter is enabled. 399 * @return {@code true} if at least one filter is enabled 400 * @since 14206 401 */ 402 public boolean hasFilters() { 403 return filterMatcher.hasFilters(); 404 } 405 406 /** 407 * Returns the list of primitives whose filtering can be affected by change in primitive 408 * @param primitives list of primitives to check 409 * @return List of primitives whose filtering can be affected by change in source primitives 410 */ 411 public static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) { 412 // Filters can use nested parent/child expression so complete tree is necessary 413 Set<OsmPrimitive> result = new HashSet<>(); 414 Stack<OsmPrimitive> stack = new Stack<>(); 415 stack.addAll(primitives); 416 417 while (!stack.isEmpty()) { 418 OsmPrimitive p = stack.pop(); 419 420 if (result.contains(p)) { 421 continue; 422 } 423 424 result.add(p); 425 426 if (p instanceof Way) { 427 for (OsmPrimitive n: ((Way) p).getNodes()) { 428 stack.push(n); 429 } 430 } else if (p instanceof Relation) { 431 for (RelationMember rm: ((Relation) p).getMembers()) { 432 stack.push(rm.getMember()); 433 } 434 } 435 436 for (OsmPrimitive ref: p.getReferrers()) { 437 stack.push(ref); 438 } 439 } 440 441 return result; 442 } 443 444 @Override 445 public void sort() { 446 Collections.sort(filters); 447 updateFilterMatcher(); 448 } 449 450 @Override 451 public void reverse() { 452 Collections.reverse(filters); 453 updateFilterMatcher(); 454 } 455}