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 @Override 284 public Filter setValue(int rowIndex, Filter filter) { 285 Filter result = filters.set(rowIndex, filter); 286 updateFilterMatcher(); 287 return result; 288 } 289 290 @Override 291 public Filter getValue(int rowIndex) { 292 return filters.get(rowIndex); 293 } 294 295 /** 296 * Draws a text on the map display that indicates that filters are active. 297 * @param g The graphics to draw that text on. 298 * @param lblOSD On Screen Display label 299 * @param header The title to display at the beginning of OSD 300 * @param footer The message to display at the bottom of OSD. Must end by {@code </html>} 301 */ 302 public void drawOSDText(Graphics2D g, OSDLabel lblOSD, String header, String footer) { 303 if (disabledCount == 0 && disabledAndHiddenCount == 0) 304 return; 305 306 String message = "<html>" + header; 307 308 if (disabledAndHiddenCount != 0) { 309 /* for correct i18n of plural forms - see #9110 */ 310 message += trn("<p><b>{0}</b> object hidden", "<p><b>{0}</b> objects hidden", disabledAndHiddenCount, disabledAndHiddenCount); 311 } 312 313 if (disabledAndHiddenCount != 0 && disabledCount != 0) { 314 message += "<br>"; 315 } 316 317 if (disabledCount != 0) { 318 /* for correct i18n of plural forms - see #9110 */ 319 message += trn("<b>{0}</b> object disabled", "<b>{0}</b> objects disabled", disabledCount, disabledCount); 320 } 321 322 message += footer; 323 324 lblOSD.setText(message); 325 lblOSD.setSize(lblOSD.getPreferredSize()); 326 327 int dx = MainApplication.getMap().mapView.getWidth() - lblOSD.getPreferredSize().width - 15; 328 int dy = 15; 329 g.translate(dx, dy); 330 lblOSD.paintComponent(g); 331 g.translate(-dx, -dy); 332 } 333 334 /** 335 * Returns the list of filters. 336 * @return the list of filters 337 */ 338 public List<Filter> getFilters() { 339 return new ArrayList<>(filters); 340 } 341 342 /** 343 * Returns the number of filters. 344 * @return the number of filters 345 */ 346 public int getFiltersCount() { 347 return filters.size(); 348 } 349 350 /** 351 * Returns the number of primitives that are disabled but not hidden. 352 * @return the number of primitives that are disabled but not hidden 353 */ 354 public int getDisabledCount() { 355 return disabledCount; 356 } 357 358 /** 359 * Returns the number of primitives that are disabled and hidden. 360 * @return the number of primitives that are disabled and hidden 361 */ 362 public int getDisabledAndHiddenCount() { 363 return disabledAndHiddenCount; 364 } 365 366 /** 367 * Determines if the filter state (normal / disabled / hidden) of any primitive has changed in the process. 368 * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process 369 */ 370 public boolean isChanged() { 371 return changed; 372 } 373 374 /** 375 * Determines if at least one filter is enabled. 376 * @return {@code true} if at least one filter is enabled 377 * @since 14206 378 */ 379 public boolean hasFilters() { 380 return filterMatcher.hasFilters(); 381 } 382 383 /** 384 * Returns the list of primitives whose filtering can be affected by change in primitive 385 * @param primitives list of primitives to check 386 * @return List of primitives whose filtering can be affected by change in source primitives 387 */ 388 public static Collection<OsmPrimitive> getAffectedPrimitives(Collection<? extends OsmPrimitive> primitives) { 389 // Filters can use nested parent/child expression so complete tree is necessary 390 Set<OsmPrimitive> result = new HashSet<>(); 391 Stack<OsmPrimitive> stack = new Stack<>(); 392 stack.addAll(primitives); 393 394 while (!stack.isEmpty()) { 395 OsmPrimitive p = stack.pop(); 396 397 if (result.contains(p)) { 398 continue; 399 } 400 401 result.add(p); 402 403 if (p instanceof Way) { 404 for (OsmPrimitive n: ((Way) p).getNodes()) { 405 stack.push(n); 406 } 407 } else if (p instanceof Relation) { 408 for (RelationMember rm: ((Relation) p).getMembers()) { 409 stack.push(rm.getMember()); 410 } 411 } 412 413 for (OsmPrimitive ref: p.getReferrers()) { 414 stack.push(ref); 415 } 416 } 417 418 return result; 419 } 420 421 @Override 422 public void sort() { 423 Collections.sort(filters); 424 updateFilterMatcher(); 425 } 426 427 @Override 428 public void reverse() { 429 Collections.reverse(filters); 430 updateFilterMatcher(); 431 } 432}