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