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