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