001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.List;
007
008import org.openstreetmap.josm.data.osm.search.SearchCompiler;
009import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
010import org.openstreetmap.josm.data.osm.search.SearchCompiler.Not;
011import org.openstreetmap.josm.data.osm.search.SearchMode;
012import org.openstreetmap.josm.data.osm.search.SearchParseError;
013import org.openstreetmap.josm.tools.SubclassFilteredCollection;
014
015/**
016 * Class that encapsulates the filter logic, i.e. applies a list of
017 * filters to a primitive.
018 *
019 * Uses {@link Match#match} to see if the filter expression matches,
020 * cares for "inverted-flag" of the filters and combines the results of all active
021 * filters.
022 *
023 * There are two major use cases:
024 *
025 * (1) Hide features that you don't like to edit but get in the way, e.g.
026 * <code>landuse</code> or power lines. It is expected, that the inverted flag
027 * if false for these kind of filters.
028 *
029 * (2) Highlight certain features, that are currently interesting and hide everything
030 * else. This can be thought of as an improved search (Ctrl-F), where you can
031 * continue editing and don't loose the current selection. It is expected that
032 * the inverted flag of the filter is true in this case.
033 *
034 * In addition to the formal application of filter rules, some magic is applied
035 * to (hopefully) match the expectations of the user:
036 *
037 * (1) non-inverted: When hiding a way, all its untagged nodes are hidden as well.
038 * This avoids a "cloud of nodes", that normally isn't useful without the
039 * corresponding way.
040 *
041 * (2) inverted: When displaying a way, we show all its nodes, although the
042 * individual nodes do not match the filter expression. The reason is, that a
043 * way without its nodes cannot be edited properly.
044 *
045 * Multipolygons and (untagged) member ways are handled in a similar way.
046 */
047public class FilterMatcher {
048
049    /**
050     * Describes quality of the filtering.
051     *
052     * Depending on the context, this can either refer to disabled or
053     * to hidden primitives.
054     *
055     * The distinction is necessary, because untagged nodes should only
056     * "inherit" their filter property from the parent way, when the
057     * parent way is hidden (or disabled) "explicitly" (i.e. by a non-inverted
058     * filter). This way, filters like
059     * <code>["child type:way", inverted, Add]</code> show the
060     * untagged way nodes, as intended.
061     *
062     * This information is only needed for ways and relations, so nodes are
063     * either <code>NOT_FILTERED</code> or <code>PASSIV</code>.
064     */
065    public enum FilterType {
066        /** no filter applies */
067        NOT_FILTERED,
068        /** at least one non-inverted filter applies */
069        EXPLICIT,
070        /** at least one filter applies, but they are all inverted filters */
071        PASSIV
072    }
073
074    private static class FilterInfo {
075        private final Match match;
076        private final boolean isDelete;
077        private final boolean isInverted;
078
079        FilterInfo(Filter filter) throws SearchParseError {
080            if (filter.mode == SearchMode.remove || filter.mode == SearchMode.in_selection) {
081                isDelete = true;
082            } else {
083                isDelete = false;
084            }
085
086            Match compiled = SearchCompiler.compile(filter);
087            this.match = filter.inverted ? new Not(compiled) : compiled;
088            this.isInverted = filter.inverted;
089        }
090    }
091
092    private final List<FilterInfo> hiddenFilters = new ArrayList<>();
093    private final List<FilterInfo> disabledFilters = new ArrayList<>();
094
095    /**
096     * Clears the current filters, and adds the given filters
097     * @param filters the filters to add
098     * @throws SearchParseError if the search expression in one of the filters cannot be parsed
099     */
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(IPrimitive 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 <T> The primitive type
172     * @param primitive the primitive to check
173     * @param hidden the level where the check is performed
174     * @return true, if at least one non-inverted filter applies to the primitive
175     */
176    private static <T extends IFilterablePrimitive> boolean isFilterExplicit(T primitive, boolean hidden) {
177        return hidden ? primitive.getHiddenType() : primitive.getDisabledType();
178    }
179
180    /**
181     * Check if all parent ways are filtered.
182     * @param <T> The primitive type
183     * @param primitive the primitive to check
184     * @param hidden parameter that indicates the minimum level of filtering:
185     * true when objects need to be hidden to count as filtered and
186     * false when it suffices to be disabled to count as filtered
187     * @return true if (a) there is at least one parent way
188     * (b) all parent ways are filtered at least at the level indicated by the
189     * parameter <code>hidden</code> and
190     * (c) at least one of the parent ways is explicitly filtered
191     */
192    private static <T extends IPrimitive & IFilterablePrimitive> boolean allParentWaysFiltered(T primitive, boolean hidden) {
193        List<? extends IPrimitive> refs = primitive.getReferrers();
194        boolean isExplicit = false;
195        for (IPrimitive p: refs) {
196            if (p instanceof IWay && p instanceof IFilterablePrimitive) {
197                if (!isFiltered(p, hidden))
198                    return false;
199                isExplicit |= isFilterExplicit((IFilterablePrimitive) p, hidden);
200            }
201        }
202        return isExplicit;
203    }
204
205    private static boolean oneParentWayNotFiltered(IPrimitive primitive, boolean hidden) {
206        return primitive.getReferrers().stream().filter(IWay.class::isInstance).map(IWay.class::cast)
207                .anyMatch(p -> !isFiltered(p, hidden));
208    }
209
210    private static boolean allParentMultipolygonsFiltered(IPrimitive primitive, boolean hidden) {
211        boolean isExplicit = false;
212        for (IRelation<?> r : new SubclassFilteredCollection<IPrimitive, IRelation<?>>(
213                primitive.getReferrers(), i -> i.isMultipolygon() && i instanceof IFilterablePrimitive)) {
214            if (!isFiltered(r, hidden))
215                return false;
216            isExplicit |= isFilterExplicit((IFilterablePrimitive) r, hidden);
217        }
218        return isExplicit;
219    }
220
221    private static boolean oneParentMultipolygonNotFiltered(IPrimitive primitive, boolean hidden) {
222        return new SubclassFilteredCollection<IPrimitive, IRelation>(primitive.getReferrers(), IPrimitive::isMultipolygon).stream()
223                .anyMatch(r -> !isFiltered(r, hidden));
224    }
225
226    private static <T extends IPrimitive & IFilterablePrimitive> FilterType test(List<FilterInfo> filters, T primitive, boolean hidden) {
227        if (primitive.isIncomplete() || primitive.isPreserved())
228            return FilterType.NOT_FILTERED;
229
230        boolean filtered = false;
231        // If the primitive is "explicitly" hidden by a non-inverted filter.
232        // Only interesting for nodes.
233        boolean explicitlyFiltered = false;
234
235        for (FilterInfo fi: filters) {
236            if (fi.isDelete) {
237                if (filtered && fi.match.match(primitive)) {
238                    filtered = false;
239                }
240            } else {
241                if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) {
242                    filtered = true;
243                    if (!fi.isInverted) {
244                        explicitlyFiltered = true;
245                    }
246                }
247            }
248        }
249
250        if (primitive instanceof INode) {
251            if (filtered) {
252                // If there is a parent way, that is not hidden, we  show the
253                // node anyway, unless there is no non-inverted filter that
254                // applies to the node directly.
255                if (explicitlyFiltered)
256                    return FilterType.PASSIV;
257                else {
258                    if (oneParentWayNotFiltered(primitive, hidden))
259                        return FilterType.NOT_FILTERED;
260                    else
261                        return FilterType.PASSIV;
262                }
263            } else {
264                if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden))
265                    // Technically not hidden by any filter, but we hide it anyway, if
266                    // it is untagged and all parent ways are hidden.
267                    return FilterType.PASSIV;
268                else
269                    return FilterType.NOT_FILTERED;
270            }
271        } else if (primitive instanceof IWay) {
272            if (filtered) {
273                if (explicitlyFiltered)
274                    return FilterType.EXPLICIT;
275                else {
276                    if (oneParentMultipolygonNotFiltered(primitive, hidden))
277                        return FilterType.NOT_FILTERED;
278                    else
279                        return FilterType.PASSIV;
280                }
281            } else {
282                if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden))
283                    return FilterType.EXPLICIT;
284                else
285                    return FilterType.NOT_FILTERED;
286            }
287        } else {
288            if (filtered)
289                return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV;
290            else
291                return FilterType.NOT_FILTERED;
292        }
293
294    }
295
296    /**
297     * Check if primitive is hidden.
298     * The filter flags for all parent objects must be set correctly, when
299     * calling this method.
300     * @param <T> The primitive type
301     * @param primitive the primitive
302     * @return FilterType.NOT_FILTERED when primitive is not hidden;
303     * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted
304     * filter that applies;
305     * FilterType.PASSIV when primitive is hidden and all filters that apply
306     * are inverted
307     */
308    public <T extends IPrimitive & IFilterablePrimitive> FilterType isHidden(T primitive) {
309        return test(hiddenFilters, primitive, true);
310    }
311
312    /**
313     * Check if primitive is disabled.
314     * The filter flags for all parent objects must be set correctly, when
315     * calling this method.
316     * @param <T> The primitive type
317     * @param primitive the primitive
318     * @return FilterType.NOT_FILTERED when primitive is not disabled;
319     * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted
320     * filter that applies;
321     * FilterType.PASSIV when primitive is disabled and all filters that apply
322     * are inverted
323     */
324    public <T extends IPrimitive & IFilterablePrimitive> FilterType isDisabled(T primitive) {
325        return test(disabledFilters, primitive, false);
326    }
327
328    /**
329     * Returns a new {@code FilterMatcher} containing the given filters.
330     * @param filters filters to add to the resulting filter matcher
331     * @return a new {@code FilterMatcher} containing the given filters
332     * @throws SearchParseError if the search expression in a filter cannot be parsed
333     * @since 12383
334     */
335    public static FilterMatcher of(Filter... filters) throws SearchParseError {
336        FilterMatcher result = new FilterMatcher();
337        for (Filter filter : filters) {
338            result.add(filter);
339        }
340        return result;
341    }
342}