source: josm/trunk/src/org/openstreetmap/josm/gui/autofilter/AutoFilterManager.java @ 13434

Last change on this file since 13434 was 13434, checked in by Don-vip, 10 months ago

see #8039, see #10456 - support read-only data layers

File size: 15.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.autofilter;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Graphics2D;
7import java.util.ArrayList;
8import java.util.Arrays;
9import java.util.Collection;
10import java.util.Comparator;
11import java.util.Iterator;
12import java.util.List;
13import java.util.Map;
14import java.util.NavigableSet;
15import java.util.Objects;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.TreeSet;
19import java.util.function.Consumer;
20import java.util.regex.Matcher;
21import java.util.regex.Pattern;
22
23import org.openstreetmap.josm.actions.mapmode.MapMode;
24import org.openstreetmap.josm.data.osm.BBox;
25import org.openstreetmap.josm.data.osm.DataSet;
26import org.openstreetmap.josm.data.osm.Filter;
27import org.openstreetmap.josm.data.osm.FilterModel;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
30import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
31import org.openstreetmap.josm.data.osm.event.DataSetListener;
32import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
33import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
34import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
35import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
36import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
37import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
38import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
39import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
40import org.openstreetmap.josm.data.preferences.BooleanProperty;
41import org.openstreetmap.josm.data.preferences.StringProperty;
42import org.openstreetmap.josm.gui.MainApplication;
43import org.openstreetmap.josm.gui.MapFrame;
44import org.openstreetmap.josm.gui.MapFrame.MapModeChangeListener;
45import org.openstreetmap.josm.gui.MapView;
46import org.openstreetmap.josm.gui.NavigatableComponent;
47import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
48import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
49import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
50import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
51import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
52import org.openstreetmap.josm.gui.layer.OsmDataLayer;
53import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
54import org.openstreetmap.josm.gui.widgets.OSDLabel;
55import org.openstreetmap.josm.spi.preferences.Config;
56import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
57import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
58import org.openstreetmap.josm.tools.Logging;
59
60/**
61 * The auto filter manager keeps track of registered auto filter rules and applies the active one on the fly,
62 * when the map contents, location or zoom changes.
63 * @since 12400
64 */
65public final class AutoFilterManager
66implements ZoomChangeListener, MapModeChangeListener, DataSetListener, PreferenceChangedListener, LayerChangeListener {
67
68    /**
69     * Property to determines if the auto filter feature is enabled.
70     */
71    public static final BooleanProperty PROP_AUTO_FILTER_ENABLED = new BooleanProperty("auto.filter.enabled", true);
72
73    /**
74     * Property to determine the current auto filter rule.
75     */
76    public static final StringProperty PROP_AUTO_FILTER_RULE = new StringProperty("auto.filter.rule", "level");
77
78    /**
79     * The unique instance.
80     */
81    private static volatile AutoFilterManager instance;
82
83    /**
84     * The buttons currently displayed in map view.
85     */
86    private final Map<String, AutoFilterButton> buttons = new TreeMap<>();
87
88    /**
89     * The list of registered auto filter rules.
90     */
91    private final List<AutoFilterRule> rules = new ArrayList<>();
92
93    /**
94     * A helper for {@link #drawOSDText(Graphics2D)}.
95     */
96    private final OSDLabel lblOSD = new OSDLabel("");
97
98    /**
99     * 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();
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}
Note: See TracBrowser for help on using the repository browser.