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.HashSet; 011import java.util.LinkedList; 012import java.util.List; 013import java.util.Set; 014import java.util.Stack; 015 016import javax.swing.JOptionPane; 017 018import org.openstreetmap.josm.data.StructUtils; 019import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry; 020import org.openstreetmap.josm.data.osm.search.SearchParseError; 021import org.openstreetmap.josm.gui.MainApplication; 022import org.openstreetmap.josm.gui.widgets.OSDLabel; 023import org.openstreetmap.josm.spi.preferences.Config; 024import org.openstreetmap.josm.tools.Logging; 025import org.openstreetmap.josm.tools.Utils; 026 027/** 028 * The model that is used both for auto and manual filters. 029 * @since 12400 030 */ 031public class FilterModel { 032 033 /** 034 * number of primitives that are disabled but not hidden 035 */ 036 private int disabledCount; 037 /** 038 * number of primitives that are disabled and hidden 039 */ 040 private int disabledAndHiddenCount; 041 /** 042 * true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process 043 */ 044 private boolean changed; 045 046 private final List<Filter> filters = new LinkedList<>(); 047 private final FilterMatcher filterMatcher = new FilterMatcher(); 048 049 private void updateFilterMatcher() { 050 filterMatcher.reset(); 051 for (Filter filter : filters) { 052 try { 053 filterMatcher.add(filter); 054 } catch (SearchParseError e) { 055 Logging.error(e); 056 JOptionPane.showMessageDialog( 057 MainApplication.getMainFrame(), 058 tr("<html>Error in filter <code>{0}</code>:<br>{1}", 059 Utils.escapeReservedCharactersHTML(Utils.shortenString(filter.text, 80)), 060 Utils.escapeReservedCharactersHTML(e.getMessage())), 061 tr("Error in filter"), 062 JOptionPane.ERROR_MESSAGE); 063 filter.enable = false; 064 } 065 } 066 } 067 068 /** 069 * Initializes the model from preferences. 070 * @param prefEntry preference key 071 */ 072 public void loadPrefs(String prefEntry) { 073 List<FilterPreferenceEntry> entries = StructUtils.getListOfStructs( 074 Config.getPref(), prefEntry, null, FilterPreferenceEntry.class); 075 if (entries != null) { 076 for (FilterPreferenceEntry e : entries) { 077 filters.add(new Filter(e)); 078 } 079 updateFilterMatcher(); 080 } 081 } 082 083 /** 084 * Saves the model to preferences. 085 * @param prefEntry preferences key 086 */ 087 public void savePrefs(String prefEntry) { 088 Collection<FilterPreferenceEntry> entries = new ArrayList<>(); 089 for (Filter flt : filters) { 090 entries.add(flt.getPreferenceEntry()); 091 } 092 StructUtils.putListOfStructs(Config.getPref(), prefEntry, entries, FilterPreferenceEntry.class); 093 } 094 095 /** 096 * Runs the filters on the current edit data set. 097 */ 098 public void executeFilters() { 099 DataSet ds = OsmDataManager.getInstance().getActiveDataSet(); 100 changed = false; 101 if (ds == null) { 102 disabledAndHiddenCount = 0; 103 disabledCount = 0; 104 changed = true; 105 } else { 106 final Collection<OsmPrimitive> deselect = new HashSet<>(); 107 108 ds.beginUpdate(); 109 try { 110 111 final Collection<OsmPrimitive> all = ds.allNonDeletedCompletePrimitives(); 112 113 changed = FilterWorker.executeFilters(all, filterMatcher); 114 115 disabledCount = 0; 116 disabledAndHiddenCount = 0; 117 // collect disabled and selected the primitives 118 for (OsmPrimitive osm : all) { 119 if (osm.isDisabled()) { 120 disabledCount++; 121 if (osm.isSelected()) { 122 deselect.add(osm); 123 } 124 if (osm.isDisabledAndHidden()) { 125 disabledAndHiddenCount++; 126 } 127 } 128 } 129 disabledCount -= disabledAndHiddenCount; 130 } finally { 131 if (changed) { 132 ds.fireFilterChanged(); 133 } 134 ds.endUpdate(); 135 } 136 137 if (!deselect.isEmpty()) { 138 ds.clearSelection(deselect); 139 } 140 } 141 if (changed) { 142 updateMap(); 143 } 144 } 145 146 /** 147 * Runs the filter on a list of primitives that are part of the edit data set. 148 * @param primitives The primitives 149 */ 150 public void executeFilters(Collection<? extends OsmPrimitive> primitives) { 151 DataSet ds = OsmDataManager.getInstance().getEditDataSet(); 152 if (ds == null) 153 return; 154 155 changed = false; 156 List<OsmPrimitive> deselect = new ArrayList<>(); 157 158 ds.beginUpdate(); 159 try { 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 } finally { 191 ds.endUpdate(); 192 } 193 194 if (!deselect.isEmpty()) { 195 ds.clearSelection(deselect); 196 } 197 if (changed) { 198 updateMap(); 199 } 200 } 201 202 private static void updateMap() { 203 MainApplication.getLayerManager().invalidateEditLayer(); 204 } 205 206 /** 207 * Clears all filtered flags from all primitives in the dataset 208 */ 209 public void clearFilterFlags() { 210 DataSet ds = OsmDataManager.getInstance().getEditDataSet(); 211 if (ds != null) { 212 FilterWorker.clearFilterFlags(ds.allPrimitives()); 213 } 214 disabledCount = 0; 215 disabledAndHiddenCount = 0; 216 } 217 218 /** 219 * Removes all filters from this model. 220 */ 221 public void clearFilters() { 222 filters.clear(); 223 updateFilterMatcher(); 224 } 225 226 /** 227 * Adds a new filter to the filter list. 228 * @param filter The new filter 229 * @return true (as specified by {@link Collection#add}) 230 */ 231 public boolean addFilter(Filter filter) { 232 filters.add(filter); 233 updateFilterMatcher(); 234 return true; 235 } 236 237 /** 238 * Moves down the filter in the given row. 239 * @param rowIndex The filter row 240 * @return true if the filter has been moved down 241 */ 242 public boolean moveDownFilter(int rowIndex) { 243 if (rowIndex >= filters.size() - 1) 244 return false; 245 filters.add(rowIndex + 1, filters.remove(rowIndex)); 246 updateFilterMatcher(); 247 return true; 248 } 249 250 /** 251 * Moves up the filter in the given row 252 * @param rowIndex The filter row 253 * @return true if the filter has been moved up 254 */ 255 public boolean moveUpFilter(int rowIndex) { 256 if (rowIndex <= 0 || rowIndex >= filters.size()) 257 return false; 258 filters.add(rowIndex - 1, filters.remove(rowIndex)); 259 updateFilterMatcher(); 260 return true; 261 } 262 263 /** 264 * Removes the filter that is displayed in the given row 265 * @param rowIndex The index of the filter to remove 266 * @return the filter previously at the specified position 267 */ 268 public Filter removeFilter(int rowIndex) { 269 Filter result = filters.remove(rowIndex); 270 updateFilterMatcher(); 271 return result; 272 } 273 274 /** 275 * Sets/replaces the filter for a given row. 276 * @param rowIndex The row index 277 * @param filter The filter that should be placed in that row 278 * @return the filter previously at the specified position 279 */ 280 public Filter setFilter(int rowIndex, Filter filter) { 281 Filter result = filters.set(rowIndex, filter); 282 updateFilterMatcher(); 283 return result; 284 } 285 286 /** 287 * Gets the filter by row index 288 * @param rowIndex The row index 289 * @return The filter in that row 290 */ 291 public Filter getFilter(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}