source: josm/trunk/src/org/openstreetmap/josm/data/osm/FilterMatcher.java @ 14206

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

fix #16698, see #15670 - make sure filters are executed (costly operation) only when necessary:

  • data changes imply execution of filters only when at least a filter is enabled
  • filter changes imply execution of filters even is no filter is enabled
  • filter dataset change events should not trigger a new filter execution!
  • Property svn:eol-style set to native
File size: 12.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.osm;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.List;
7
8import org.openstreetmap.josm.data.osm.search.SearchCompiler;
9import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
10import org.openstreetmap.josm.data.osm.search.SearchCompiler.Not;
11import org.openstreetmap.josm.data.osm.search.SearchMode;
12import org.openstreetmap.josm.data.osm.search.SearchParseError;
13import org.openstreetmap.josm.tools.SubclassFilteredCollection;
14
15/**
16 * Class that encapsulates the filter logic, i.e. applies a list of
17 * filters to a primitive.
18 *
19 * Uses {@link Match#match} to see if the filter expression matches,
20 * cares for "inverted-flag" of the filters and combines the results of all active
21 * filters.
22 *
23 * There are two major use cases:
24 *
25 * (1) Hide features that you don't like to edit but get in the way, e.g.
26 * <code>landuse</code> or power lines. It is expected, that the inverted flag
27 * if false for these kind of filters.
28 *
29 * (2) Highlight certain features, that are currently interesting and hide everything
30 * else. This can be thought of as an improved search (Ctrl-F), where you can
31 * continue editing and don't loose the current selection. It is expected that
32 * the inverted flag of the filter is true in this case.
33 *
34 * In addition to the formal application of filter rules, some magic is applied
35 * to (hopefully) match the expectations of the user:
36 *
37 * (1) non-inverted: When hiding a way, all its untagged nodes are hidden as well.
38 * This avoids a "cloud of nodes", that normally isn't useful without the
39 * corresponding way.
40 *
41 * (2) inverted: When displaying a way, we show all its nodes, although the
42 * individual nodes do not match the filter expression. The reason is, that a
43 * way without its nodes cannot be edited properly.
44 *
45 * Multipolygons and (untagged) member ways are handled in a similar way.
46 */
47public class FilterMatcher {
48
49    /**
50     * Describes quality of the filtering.
51     *
52     * Depending on the context, this can either refer to disabled or
53     * to hidden primitives.
54     *
55     * The distinction is necessary, because untagged nodes should only
56     * "inherit" their filter property from the parent way, when the
57     * parent way is hidden (or disabled) "explicitly" (i.e. by a non-inverted
58     * filter). This way, filters like
59     * <code>["child type:way", inverted, Add]</code> show the
60     * untagged way nodes, as intended.
61     *
62     * This information is only needed for ways and relations, so nodes are
63     * either <code>NOT_FILTERED</code> or <code>PASSIV</code>.
64     */
65    public enum FilterType {
66        /** no filter applies */
67        NOT_FILTERED,
68        /** at least one non-inverted filter applies */
69        EXPLICIT,
70        /** at least one filter applies, but they are all inverted filters */
71        PASSIV
72    }
73
74    private static class FilterInfo {
75        private final Match match;
76        private final boolean isDelete;
77        private final boolean isInverted;
78
79        FilterInfo(Filter filter) throws SearchParseError {
80            if (filter.mode == SearchMode.remove || filter.mode == SearchMode.in_selection) {
81                isDelete = true;
82            } else {
83                isDelete = false;
84            }
85
86            Match compiled = SearchCompiler.compile(filter);
87            this.match = filter.inverted ? new Not(compiled) : compiled;
88            this.isInverted = filter.inverted;
89        }
90    }
91
92    private final List<FilterInfo> hiddenFilters = new ArrayList<>();
93    private final List<FilterInfo> disabledFilters = new ArrayList<>();
94
95    /**
96     * Clears the current filters, and adds the given filters
97     * @param filters the filters to add
98     * @throws SearchParseError if the search expression in one of the filters cannot be parsed
99     */
100    public void update(Collection<Filter> filters) throws SearchParseError {
101        reset();
102        for (Filter filter : filters) {
103            add(filter);
104        }
105    }
106
107    /**
108     * Clears the filters in use.
109     */
110    public void reset() {
111        hiddenFilters.clear();
112        disabledFilters.clear();
113    }
114
115    /**
116     * Determines if at least one filter is enabled.
117     * @return {@code true} if at least one filter is enabled
118     * @since 14206
119     */
120    public boolean hasFilters() {
121        return !hiddenFilters.isEmpty() || !disabledFilters.isEmpty();
122    }
123
124    /**
125     * Adds a filter to the currently used filters
126     * @param filter the filter to add
127     * @throws SearchParseError if the search expression in the filter cannot be parsed
128     */
129    public void add(final Filter filter) throws SearchParseError {
130        if (!filter.enable) {
131            return;
132        }
133
134        FilterInfo fi = new FilterInfo(filter);
135        if (fi.isDelete) {
136            if (filter.hiding) {
137                // Remove only hide flag
138                hiddenFilters.add(fi);
139            } else {
140                // Remove both flags
141                disabledFilters.add(fi);
142                hiddenFilters.add(fi);
143            }
144        } else {
145            if (filter.mode == SearchMode.replace && filter.hiding) {
146                hiddenFilters.clear();
147                disabledFilters.clear();
148            }
149
150            disabledFilters.add(fi);
151            if (filter.hiding) {
152                hiddenFilters.add(fi);
153            }
154        }
155    }
156
157    /**
158     * Check if primitive is filtered.
159     * @param primitive the primitive to check
160     * @param hidden the minimum level required for the primitive to count as filtered
161     * @return when hidden is true, returns whether the primitive is hidden
162     * when hidden is false, returns whether the primitive is disabled or hidden
163     */
164    private static boolean isFiltered(OsmPrimitive primitive, boolean hidden) {
165        return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled();
166    }
167
168    /**
169     * Check if primitive is hidden explicitly.
170     * Only used for ways and relations.
171     * @param primitive the primitive to check
172     * @param hidden the level where the check is performed
173     * @return true, if at least one non-inverted filter applies to the primitive
174     */
175    private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) {
176        return hidden ? primitive.getHiddenType() : primitive.getDisabledType();
177    }
178
179    /**
180     * Check if all parent ways are filtered.
181     * @param primitive the primitive to check
182     * @param hidden parameter that indicates the minimum level of filtering:
183     * true when objects need to be hidden to count as filtered and
184     * false when it suffices to be disabled to count as filtered
185     * @return true if (a) there is at least one parent way
186     * (b) all parent ways are filtered at least at the level indicated by the
187     * parameter <code>hidden</code> and
188     * (c) at least one of the parent ways is explicitly filtered
189     */
190    private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) {
191        List<OsmPrimitive> refs = primitive.getReferrers();
192        boolean isExplicit = false;
193        for (OsmPrimitive p: refs) {
194            if (p instanceof Way) {
195                if (!isFiltered(p, hidden))
196                    return false;
197                isExplicit |= isFilterExplicit(p, hidden);
198            }
199        }
200        return isExplicit;
201    }
202
203    private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) {
204        List<OsmPrimitive> refs = primitive.getReferrers();
205        for (OsmPrimitive p: refs) {
206            if (p instanceof Way && !isFiltered(p, hidden))
207                return true;
208        }
209
210        return false;
211    }
212
213    private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) {
214        boolean isExplicit = false;
215        for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>(
216                primitive.getReferrers(), OsmPrimitive::isMultipolygon)) {
217            if (!isFiltered(r, hidden))
218                return false;
219            isExplicit |= isFilterExplicit(r, hidden);
220        }
221        return isExplicit;
222    }
223
224    private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) {
225        for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>(
226                primitive.getReferrers(), OsmPrimitive::isMultipolygon)) {
227            if (!isFiltered(r, hidden))
228                return true;
229        }
230        return false;
231    }
232
233    private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) {
234        if (primitive.isIncomplete() || primitive.isPreserved())
235            return FilterType.NOT_FILTERED;
236
237        boolean filtered = false;
238        // If the primitive is "explicitly" hidden by a non-inverted filter.
239        // Only interesting for nodes.
240        boolean explicitlyFiltered = false;
241
242        for (FilterInfo fi: filters) {
243            if (fi.isDelete) {
244                if (filtered && fi.match.match(primitive)) {
245                    filtered = false;
246                }
247            } else {
248                if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) {
249                    filtered = true;
250                    if (!fi.isInverted) {
251                        explicitlyFiltered = true;
252                    }
253                }
254            }
255        }
256
257        if (primitive instanceof Node) {
258            if (filtered) {
259                // If there is a parent way, that is not hidden, we  show the
260                // node anyway, unless there is no non-inverted filter that
261                // applies to the node directly.
262                if (explicitlyFiltered)
263                    return FilterType.PASSIV;
264                else {
265                    if (oneParentWayNotFiltered(primitive, hidden))
266                        return FilterType.NOT_FILTERED;
267                    else
268                        return FilterType.PASSIV;
269                }
270            } else {
271                if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden))
272                    // Technically not hidden by any filter, but we hide it anyway, if
273                    // it is untagged and all parent ways are hidden.
274                    return FilterType.PASSIV;
275                else
276                    return FilterType.NOT_FILTERED;
277            }
278        } else if (primitive instanceof Way) {
279            if (filtered) {
280                if (explicitlyFiltered)
281                    return FilterType.EXPLICIT;
282                else {
283                    if (oneParentMultipolygonNotFiltered(primitive, hidden))
284                        return FilterType.NOT_FILTERED;
285                    else
286                        return FilterType.PASSIV;
287                }
288            } else {
289                if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden))
290                    return FilterType.EXPLICIT;
291                else
292                    return FilterType.NOT_FILTERED;
293            }
294        } else {
295            if (filtered)
296                return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV;
297            else
298                return FilterType.NOT_FILTERED;
299        }
300
301    }
302
303    /**
304     * Check if primitive is hidden.
305     * The filter flags for all parent objects must be set correctly, when
306     * calling this method.
307     * @param primitive the primitive
308     * @return FilterType.NOT_FILTERED when primitive is not hidden;
309     * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted
310     * filter that applies;
311     * FilterType.PASSIV when primitive is hidden and all filters that apply
312     * are inverted
313     */
314    public FilterType isHidden(OsmPrimitive primitive) {
315        return test(hiddenFilters, primitive, true);
316    }
317
318    /**
319     * Check if primitive is disabled.
320     * The filter flags for all parent objects must be set correctly, when
321     * calling this method.
322     * @param primitive the primitive
323     * @return FilterType.NOT_FILTERED when primitive is not disabled;
324     * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted
325     * filter that applies;
326     * FilterType.PASSIV when primitive is disabled and all filters that apply
327     * are inverted
328     */
329    public FilterType isDisabled(OsmPrimitive primitive) {
330        return test(disabledFilters, primitive, false);
331    }
332
333    /**
334     * Returns a new {@code FilterMatcher} containing the given filters.
335     * @param filters filters to add to the resulting filter matcher
336     * @return a new {@code FilterMatcher} containing the given filters
337     * @throws SearchParseError if the search expression in a filter cannot be parsed
338     * @since 12383
339     */
340    public static FilterMatcher of(Filter... filters) throws SearchParseError {
341        FilterMatcher result = new FilterMatcher();
342        for (Filter filter : filters) {
343            result.add(filter);
344        }
345        return result;
346    }
347}
Note: See TracBrowser for help on using the repository browser.