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}