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.actions.search.SearchAction.SearchMode;
009import org.openstreetmap.josm.actions.search.SearchCompiler;
010import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
011import org.openstreetmap.josm.actions.search.SearchCompiler.Not;
012import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
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        final Match match;
076        final boolean isDelete;
077        final boolean isInverted;
078
079        FilterInfo(Filter filter) throws ParseError {
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.text, filter.caseSensitive, filter.regexSearch);
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    public void update(Collection<Filter> filters) throws ParseError {
096        hiddenFilters.clear();
097        disabledFilters.clear();
098
099        for (Filter filter: filters) {
100
101            if (!filter.enable) {
102                continue;
103            }
104
105            FilterInfo fi = new FilterInfo(filter);
106            if (fi.isDelete) {
107                if (filter.hiding) {
108                    // Remove only hide flag
109                    hiddenFilters.add(fi);
110                } else {
111                    // Remove both flags
112                    disabledFilters.add(fi);
113                    hiddenFilters.add(fi);
114                }
115            } else {
116                if (filter.mode == SearchMode.replace) {
117                    if (filter.hiding) {
118                        hiddenFilters.clear();
119                        disabledFilters.clear();
120                    }
121                }
122
123                disabledFilters.add(fi);
124                if (filter.hiding) {
125                    hiddenFilters.add(fi);
126                }
127            }
128        }
129    }
130
131    /**
132     * Check if primitive is filtered.
133     * @param primitive the primitive to check
134     * @param hidden the minimum level required for the primitive to count as filtered
135     * @return when hidden is true, returns whether the primitive is hidden
136     * when hidden is false, returns whether the primitive is disabled or hidden
137     */
138    private boolean isFiltered(OsmPrimitive primitive, boolean hidden) {
139        return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled();
140    }
141
142    /**
143     * Check if primitive is hidden explicitly.
144     * Only used for ways and relations.
145     * @param primitive the primitive to check
146     * @param hidden the level where the check is performed
147     * @return true, if at least one non-inverted filter applies to the primitive
148     */
149    private boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) {
150        return hidden ? primitive.getHiddenType() : primitive.getDisabledType();
151    }
152
153    /**
154     * Check if all parent ways are filtered.
155     * @param primitive the primitive to check
156     * @param hidden parameter that indicates the minimum level of filtering:
157     * true when objects need to be hidden to count as filtered and
158     * false when it suffices to be disabled to count as filtered
159     * @return true if (a) there is at least one parent way
160     * (b) all parent ways are filtered at least at the level indicated by the
161     * parameter <code>hidden</code> and
162     * (c) at least one of the parent ways is explicitly filtered
163     */
164    private boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) {
165        List<OsmPrimitive> refs = primitive.getReferrers();
166        boolean isExplicit = false;
167        for (OsmPrimitive p: refs) {
168            if (p instanceof Way) {
169                if (!isFiltered(p, hidden))
170                    return false;
171                isExplicit |= isFilterExplicit(p, hidden);
172            }
173        }
174        return isExplicit;
175    }
176
177    private boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) {
178        List<OsmPrimitive> refs = primitive.getReferrers();
179        for (OsmPrimitive p: refs) {
180            if (p instanceof Way && !isFiltered(p, hidden))
181                return true;
182        }
183
184        return false;
185    }
186
187    private boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) {
188        boolean isExplicit = false;
189        for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>(
190                primitive.getReferrers(), OsmPrimitive.multipolygonPredicate)) {
191            if (!isFiltered(r, hidden))
192                return false;
193            isExplicit |= isFilterExplicit(r, hidden);
194        }
195        return isExplicit;
196    }
197
198    private boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) {
199        for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>(
200                primitive.getReferrers(), OsmPrimitive.multipolygonPredicate)) {
201            if (!isFiltered(r, hidden))
202                return true;
203        }
204        return false;
205    }
206
207    private FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) {
208
209        if (primitive.isIncomplete())
210            return FilterType.NOT_FILTERED;
211
212        boolean filtered = false;
213        // If the primitive is "explicitly" hidden by a non-inverted filter.
214        // Only interesting for nodes.
215        boolean explicitlyFiltered = false;
216
217        for (FilterInfo fi: filters) {
218            if (fi.isDelete) {
219                if (filtered && fi.match.match(primitive)) {
220                    filtered = false;
221                }
222            } else {
223                if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) {
224                    filtered = true;
225                    if (!fi.isInverted) {
226                        explicitlyFiltered = true;
227                    }
228                }
229            }
230        }
231
232        if (primitive instanceof Node) {
233            if (filtered) {
234                // If there is a parent way, that is not hidden, we  show the
235                // node anyway, unless there is no non-inverted filter that
236                // applies to the node directly.
237                if (explicitlyFiltered)
238                    return FilterType.PASSIV;
239                else {
240                    if (oneParentWayNotFiltered(primitive, hidden))
241                        return FilterType.NOT_FILTERED;
242                    else
243                        return FilterType.PASSIV;
244                }
245            } else {
246                if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden))
247                    // Technically not hidden by any filter, but we hide it anyway, if
248                    // it is untagged and all parent ways are hidden.
249                    return FilterType.PASSIV;
250                else
251                    return FilterType.NOT_FILTERED;
252            }
253        } else if (primitive instanceof Way) {
254            if (filtered) {
255                if (explicitlyFiltered)
256                    return FilterType.EXPLICIT;
257                else {
258                    if (oneParentMultipolygonNotFiltered(primitive, hidden))
259                        return FilterType.NOT_FILTERED;
260                    else
261                        return FilterType.PASSIV;
262                }
263            } else {
264                if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden))
265                    return FilterType.EXPLICIT;
266                else
267                    return FilterType.NOT_FILTERED;
268            }
269        } else {
270            if (filtered)
271                return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV;
272            else
273                return FilterType.NOT_FILTERED;
274        }
275
276    }
277
278    /**
279     * Check if primitive is hidden.
280     * The filter flags for all parent objects must be set correctly, when
281     * calling this method.
282     * @param primitive the primitive
283     * @return FilterType.NOT_FILTERED when primitive is not hidden;
284     * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted
285     * filter that applies;
286     * FilterType.PASSIV when primitive is hidden and all filters that apply
287     * are inverted
288     */
289    public FilterType isHidden(OsmPrimitive primitive) {
290        return test(hiddenFilters, primitive, true);
291    }
292
293    /**
294     * Check if primitive is disabled.
295     * The filter flags for all parent objects must be set correctly, when
296     * calling this method.
297     * @param primitive the primitive
298     * @return FilterType.NOT_FILTERED when primitive is not disabled;
299     * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted
300     * filter that applies;
301     * FilterType.PASSIV when primitive is disabled and all filters that apply
302     * are inverted
303     */
304    public FilterType isDisabled(OsmPrimitive primitive) {
305        return test(disabledFilters, primitive, false);
306    }
307
308}