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}