001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.data.osm;
003    
004    import java.util.ArrayList;
005    import java.util.Collection;
006    import java.util.List;
007    
008    import org.openstreetmap.josm.actions.search.SearchAction.SearchMode;
009    import org.openstreetmap.josm.actions.search.SearchCompiler;
010    import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
011    import org.openstreetmap.josm.actions.search.SearchCompiler.Not;
012    import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
013    import 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 SearchCompiler.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     */
047    public 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<FilterInfo>();
093        private final List<FilterInfo> disabledFilters = new ArrayList<FilterInfo>();
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    }