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