001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.autofilter; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Graphics2D; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.List; 012import java.util.Map; 013import java.util.NavigableSet; 014import java.util.Objects; 015import java.util.TreeMap; 016import java.util.TreeSet; 017import java.util.function.Consumer; 018 019import org.openstreetmap.josm.actions.mapmode.MapMode; 020import org.openstreetmap.josm.data.osm.BBox; 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.Filter; 023import org.openstreetmap.josm.data.osm.FilterModel; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 026import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 027import org.openstreetmap.josm.data.osm.event.DataSetListener; 028import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 029import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode; 030import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 031import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 032import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 033import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 034import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 035import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 036import org.openstreetmap.josm.data.osm.search.SearchCompiler; 037import org.openstreetmap.josm.data.osm.search.SearchCompiler.MatchSupplier; 038import org.openstreetmap.josm.data.preferences.BooleanProperty; 039import org.openstreetmap.josm.data.preferences.StringProperty; 040import org.openstreetmap.josm.gui.MainApplication; 041import org.openstreetmap.josm.gui.MapFrame; 042import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener; 043import org.openstreetmap.josm.gui.MapView; 044import org.openstreetmap.josm.gui.NavigatableComponent; 045import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 046import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent; 047import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener; 048import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent; 049import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent; 050import org.openstreetmap.josm.gui.layer.OsmDataLayer; 051import org.openstreetmap.josm.gui.mappaint.mapcss.Selector; 052import org.openstreetmap.josm.gui.widgets.OSDLabel; 053import org.openstreetmap.josm.spi.preferences.Config; 054import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent; 055import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener; 056 057/** 058 * The auto filter manager keeps track of registered auto filter rules and applies the active one on the fly, 059 * when the map contents, location or zoom changes. 060 * @since 12400 061 */ 062public final class AutoFilterManager 063implements ZoomChangeListener, MapModeChangeListener, DataSetListener, PreferenceChangedListener, LayerChangeListener { 064 065 /** 066 * Property to determines if the auto filter feature is enabled. 067 */ 068 public static final BooleanProperty PROP_AUTO_FILTER_ENABLED = new BooleanProperty("auto.filter.enabled", true); 069 070 /** 071 * Property to determine the current auto filter rule. 072 */ 073 public static final StringProperty PROP_AUTO_FILTER_RULE = new StringProperty("auto.filter.rule", "level"); 074 075 /** 076 * The unique instance. 077 */ 078 private static volatile AutoFilterManager instance; 079 080 /** 081 * The buttons currently displayed in map view. 082 */ 083 private final Map<Integer, AutoFilterButton> buttons = new TreeMap<>(); 084 085 /** 086 * The list of registered auto filter rules. 087 */ 088 private final List<AutoFilterRule> rules = new ArrayList<>(); 089 090 /** 091 * A helper for {@link #drawOSDText(Graphics2D)}. 092 */ 093 private final OSDLabel lblOSD = new OSDLabel(""); 094 095 /** 096 * The filter model. 097 */ 098 private final FilterModel model = new FilterModel(); 099 100 /** 101 * The currently enabled rule, if any. 102 */ 103 AutoFilterRule enabledRule; 104 105 /** 106 * The currently selected auto filter, if any. 107 */ 108 private AutoFilter currentAutoFilter; 109 110 /** 111 * Returns the unique instance. 112 * @return the unique instance 113 */ 114 public static AutoFilterManager getInstance() { 115 if (instance == null) { 116 instance = new AutoFilterManager(); 117 } 118 return instance; 119 } 120 121 private AutoFilterManager() { 122 MapFrame.addMapModeChangeListener(this); 123 Config.getPref().addPreferenceChangeListener(this); 124 NavigatableComponent.addZoomChangeListener(this); 125 MainApplication.getLayerManager().addLayerChangeListener(this); 126 DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT_CONSOLIDATED); 127 registerAutoFilterRules(AutoFilterRule.defaultRules()); 128 } 129 130 private synchronized void updateButtons() { 131 MapFrame map = MainApplication.getMap(); 132 if (enabledRule != null && map != null 133 && enabledRule.getMinZoomLevel() <= Selector.GeneralSelector.scale2level(map.mapView.getDist100Pixel())) { 134 // Retrieve the values from current rule visible on screen 135 NavigableSet<Integer> values = getNumericValues(); 136 // Make sure current auto filter button remains visible even if no data is found, to allow user to disable it 137 if (currentAutoFilter != null) { 138 values.add(currentAutoFilter.getFilter().value); 139 } 140 if (!values.equals(buttons.keySet())) { 141 removeAllButtons(); 142 addNewButtons(values); 143 } 144 } 145 } 146 147 static class CompiledFilter extends Filter implements MatchSupplier { 148 final AutoFilterRule rule; 149 final int value; 150 151 CompiledFilter(AutoFilterRule rule, int value) { 152 this.rule = rule; 153 this.value = value; 154 this.enable = true; 155 this.inverted = true; 156 this.text = rule.getKey() + "=" + rule.formatValue(value); 157 } 158 159 @Override 160 public SearchCompiler.Match get() { 161 return new Match(rule, value); 162 } 163 164 @Override 165 public int hashCode() { 166 return 31 * super.hashCode() + Objects.hash(rule, value); 167 } 168 169 @Override 170 public boolean equals(Object obj) { 171 if (this == obj) 172 return true; 173 if (!super.equals(obj) || getClass() != obj.getClass()) 174 return false; 175 CompiledFilter other = (CompiledFilter) obj; 176 return Objects.equals(rule, other.rule) && value == other.value; 177 } 178 } 179 180 static class Match extends SearchCompiler.Match { 181 final AutoFilterRule rule; 182 final int value; 183 184 Match(AutoFilterRule rule, int value) { 185 this.rule = rule; 186 this.value = value; 187 } 188 189 @Override 190 public boolean match(OsmPrimitive osm) { 191 return rule.getTagValuesForPrimitive(osm).anyMatch(v -> v == value); 192 } 193 194 @Override 195 public boolean equals(Object o) { 196 if (this == o) return true; 197 if (o == null || getClass() != o.getClass()) return false; 198 Match match = (Match) o; 199 return value == match.value && 200 Objects.equals(rule, match.rule); 201 } 202 203 @Override 204 public int hashCode() { 205 return Objects.hash(rule, value); 206 } 207 } 208 209 private synchronized void addNewButtons(NavigableSet<Integer> values) { 210 if (values.isEmpty()) { 211 return; 212 } 213 int i = 0; 214 int maxWidth = 16; 215 final AutoFilterButton keyButton = AutoFilterButton.forOsmKey(enabledRule.getKey()); 216 addButton(keyButton, Integer.MIN_VALUE, i++); 217 for (final Integer value : values.descendingSet()) { 218 CompiledFilter filter = new CompiledFilter(enabledRule, value); 219 String label = enabledRule.formatValue(value); 220 AutoFilter autoFilter = new AutoFilter(label, filter.text, filter); 221 AutoFilterButton button = new AutoFilterButton(autoFilter); 222 if (autoFilter.equals(currentAutoFilter)) { 223 button.getModel().setPressed(true); 224 } 225 maxWidth = Math.max(maxWidth, button.getPreferredSize().width); 226 addButton(button, value, i++); 227 } 228 for (AutoFilterButton b : buttons.values()) { 229 b.setSize(b == keyButton ? b.getPreferredSize().width : maxWidth, 20); 230 } 231 MainApplication.getMap().mapView.validate(); 232 } 233 234 private void addButton(AutoFilterButton button, int value, int i) { 235 MapView mapView = MainApplication.getMap().mapView; 236 buttons.put(value, button); 237 mapView.add(button).setLocation(3, 60 + 22*i); 238 } 239 240 private void removeAllButtons() { 241 MapFrame map = MainApplication.getMap(); 242 if (map != null) { 243 buttons.values().forEach(map.mapView::remove); 244 } 245 buttons.clear(); 246 } 247 248 private synchronized NavigableSet<Integer> getNumericValues() { 249 DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 250 if (ds == null) { 251 return Collections.emptyNavigableSet(); 252 } 253 BBox bbox = MainApplication.getMap().mapView.getState().getViewArea().getLatLonBoundsBox().toBBox(); 254 NavigableSet<Integer> values = new TreeSet<>(); 255 Consumer<OsmPrimitive> consumer = o -> enabledRule.getTagValuesForPrimitive(o).forEach(values::add); 256 ds.searchNodes(bbox).forEach(consumer); 257 ds.searchWays(bbox).forEach(consumer); 258 ds.searchRelations(bbox).forEach(consumer); 259 return values; 260 } 261 262 @Override 263 public void zoomChanged() { 264 updateButtons(); 265 } 266 267 @Override 268 public void dataChanged(DataChangedEvent event) { 269 updateFiltersFull(); 270 } 271 272 @Override 273 public void nodeMoved(NodeMovedEvent event) { 274 updateFiltersFull(); 275 } 276 277 @Override 278 public void otherDatasetChange(AbstractDatasetChangedEvent event) { 279 updateFiltersFull(); 280 } 281 282 @Override 283 public void primitivesAdded(PrimitivesAddedEvent event) { 284 updateFiltersEvent(event, false); 285 updateButtons(); 286 } 287 288 @Override 289 public void primitivesRemoved(PrimitivesRemovedEvent event) { 290 updateFiltersFull(); 291 updateButtons(); 292 } 293 294 @Override 295 public void relationMembersChanged(RelationMembersChangedEvent event) { 296 updateFiltersEvent(event, true); 297 } 298 299 @Override 300 public void tagsChanged(TagsChangedEvent event) { 301 updateFiltersEvent(event, true); 302 updateButtons(); 303 } 304 305 @Override 306 public void wayNodesChanged(WayNodesChangedEvent event) { 307 updateFiltersEvent(event, true); 308 } 309 310 @Override 311 public void mapModeChange(MapMode oldMapMode, MapMode newMapMode) { 312 updateFiltersFull(); 313 } 314 315 private synchronized void updateFiltersFull() { 316 if (currentAutoFilter != null) { 317 model.executeFilters(); 318 } 319 } 320 321 private synchronized void updateFiltersEvent(AbstractDatasetChangedEvent event, boolean affectedOnly) { 322 if (currentAutoFilter != null) { 323 Collection<? extends OsmPrimitive> prims = event.getPrimitives(); 324 model.executeFilters(affectedOnly ? FilterModel.getAffectedPrimitives(prims) : prims); 325 } 326 } 327 328 /** 329 * Registers new auto filter rule(s). 330 * @param filterRules new auto filter rules. Must not be null 331 * @return {@code true} if the list changed as a result of the call 332 * @throws NullPointerException if {@code filterRules} is null 333 */ 334 public synchronized boolean registerAutoFilterRules(AutoFilterRule... filterRules) { 335 return rules.addAll(Arrays.asList(filterRules)); 336 } 337 338 /** 339 * Unregisters an auto filter rule. 340 * @param rule auto filter rule to remove. Must not be null 341 * @return {@code true} if the list contained the specified rule 342 * @throws NullPointerException if {@code rule} is null 343 */ 344 public synchronized boolean unregisterAutoFilterRule(AutoFilterRule rule) { 345 return rules.remove(Objects.requireNonNull(rule, "rule")); 346 } 347 348 /** 349 * Returns the list of registered auto filter rules. 350 * @return the list of registered rules 351 */ 352 public synchronized List<AutoFilterRule> getAutoFilterRules() { 353 return new ArrayList<>(rules); 354 } 355 356 /** 357 * Returns the auto filter rule defined for the given OSM key. 358 * @param key OSM key used to identify rule. Can't be null. 359 * @return the auto filter rule defined for the given OSM key, or null 360 * @throws NullPointerException if key is null 361 */ 362 public synchronized AutoFilterRule getAutoFilterRule(String key) { 363 return rules.stream() 364 .filter(r -> Objects.equals(key, r.getKey())) 365 .findFirst().orElse(null); 366 } 367 368 /** 369 * Sets the currently enabled auto filter rule to the one defined for the given OSM key. 370 * @param key OSM key used to identify new rule to enable. Null to disable the auto filter feature. 371 */ 372 public synchronized void enableAutoFilterRule(String key) { 373 enableAutoFilterRule(key == null ? null : getAutoFilterRule(key)); 374 } 375 376 /** 377 * Sets the currently enabled auto filter rule. 378 * @param rule new rule to enable. Null to disable the auto filter feature. 379 */ 380 public synchronized void enableAutoFilterRule(AutoFilterRule rule) { 381 enabledRule = rule; 382 } 383 384 /** 385 * Returns the currently selected auto filter, if any. 386 * @return the currently selected auto filter, or null 387 */ 388 public synchronized AutoFilter getCurrentAutoFilter() { 389 return currentAutoFilter; 390 } 391 392 /** 393 * Sets the currently selected auto filter, if any. 394 * @param autoFilter the currently selected auto filter, or null 395 */ 396 public synchronized void setCurrentAutoFilter(AutoFilter autoFilter) { 397 model.clearFilters(); 398 currentAutoFilter = autoFilter; 399 if (autoFilter != null) { 400 model.addFilter(autoFilter.getFilter()); 401 model.executeFilters(); 402 if (model.isChanged()) { 403 OsmDataLayer dataLayer = MainApplication.getLayerManager().getActiveDataLayer(); 404 if (dataLayer != null) { 405 dataLayer.invalidate(); 406 } 407 } 408 } 409 } 410 411 /** 412 * Draws a text on the map display that indicates that filters are active. 413 * @param g The graphics to draw that text on. 414 */ 415 public synchronized void drawOSDText(Graphics2D g) { 416 model.drawOSDText(g, lblOSD, 417 tr("<h2>Filter active: {0}</h2>", currentAutoFilter.getFilter().text), 418 tr("</p><p>Click again on filter button to see all objects.</p></html>")); 419 } 420 421 private void resetCurrentAutoFilter() { 422 setCurrentAutoFilter(null); 423 removeAllButtons(); 424 MapFrame map = MainApplication.getMap(); 425 if (map != null) { 426 map.filterDialog.getFilterModel().executeFilters(true); 427 } 428 } 429 430 @Override 431 public void preferenceChanged(PreferenceChangeEvent e) { 432 if (e.getKey().equals(PROP_AUTO_FILTER_ENABLED.getKey())) { 433 if (PROP_AUTO_FILTER_ENABLED.get()) { 434 enableAutoFilterRule(PROP_AUTO_FILTER_RULE.get()); 435 updateButtons(); 436 } else { 437 enableAutoFilterRule((AutoFilterRule) null); 438 resetCurrentAutoFilter(); 439 } 440 } else if (e.getKey().equals(PROP_AUTO_FILTER_RULE.getKey())) { 441 enableAutoFilterRule(PROP_AUTO_FILTER_RULE.get()); 442 resetCurrentAutoFilter(); 443 updateButtons(); 444 } 445 } 446 447 @Override 448 public void layerAdded(LayerAddEvent e) { 449 // Do nothing 450 } 451 452 @Override 453 public void layerRemoving(LayerRemoveEvent e) { 454 if (MainApplication.getLayerManager().getActiveDataLayer() == null) { 455 resetCurrentAutoFilter(); 456 } 457 } 458 459 @Override 460 public void layerOrderChanged(LayerOrderChangeEvent e) { 461 // Do nothing 462 } 463}