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

Last change on this file was 18801, checked in by taylor.smock, 10 months ago

Fix #22832: Code cleanup and some simplification, documentation fixes (patch by gaben)

There should not be any functional changes in this patch; it is intended to do
the following:

  • Simplify and cleanup code (example: Arrays.asList(item) -> Collections.singletonList(item))
  • Fix typos in documentation (which also corrects the documentation to match what actually happens, in some cases)
  • Property svn:eol-style set to native
File size: 13.1 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 isDelete = filter.mode == SearchMode.remove || filter.mode == SearchMode.in_selection;
81
82 Match compiled = SearchCompiler.compile(filter);
83 this.match = filter.inverted ? new Not(compiled) : compiled;
84 this.isInverted = filter.inverted;
85 }
86 }
87
88 private final List<FilterInfo> hiddenFilters = new ArrayList<>();
89 private final List<FilterInfo> disabledFilters = new ArrayList<>();
90
91 /**
92 * Clears the current filters, and adds the given filters
93 * @param filters the filters to add
94 * @throws SearchParseError if the search expression in one of the filters cannot be parsed
95 */
96 public void update(Collection<Filter> filters) throws SearchParseError {
97 reset();
98 for (Filter filter : filters) {
99 add(filter);
100 }
101 }
102
103 /**
104 * Clears the filters in use.
105 */
106 public void reset() {
107 hiddenFilters.clear();
108 disabledFilters.clear();
109 }
110
111 /**
112 * Determines if at least one filter is enabled.
113 * @return {@code true} if at least one filter is enabled
114 * @since 14206
115 */
116 public boolean hasFilters() {
117 return !hiddenFilters.isEmpty() || !disabledFilters.isEmpty();
118 }
119
120 /**
121 * Adds a filter to the currently used filters
122 * @param filter the filter to add
123 * @throws SearchParseError if the search expression in the filter cannot be parsed
124 */
125 public void add(final Filter filter) throws SearchParseError {
126 if (!filter.enable) {
127 return;
128 }
129
130 FilterInfo fi = new FilterInfo(filter);
131 if (fi.isDelete) {
132 if (filter.hiding) {
133 // Remove only hide flag
134 hiddenFilters.add(fi);
135 } else {
136 // Remove both flags
137 disabledFilters.add(fi);
138 hiddenFilters.add(fi);
139 }
140 } else {
141 if (filter.mode == SearchMode.replace && filter.hiding) {
142 hiddenFilters.clear();
143 disabledFilters.clear();
144 }
145
146 disabledFilters.add(fi);
147 if (filter.hiding) {
148 hiddenFilters.add(fi);
149 }
150 }
151 }
152
153 /**
154 * Check if primitive is filtered.
155 * @param primitive the primitive to check
156 * @param hidden the minimum level required for the primitive to count as filtered
157 * @return when hidden is true, returns whether the primitive is hidden
158 * when hidden is false, returns whether the primitive is disabled or hidden
159 */
160 private static boolean isFiltered(IPrimitive primitive, boolean hidden) {
161 return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled();
162 }
163
164 /**
165 * Check if primitive is hidden explicitly.
166 * Only used for ways and relations.
167 * @param <T> The primitive type
168 * @param primitive the primitive to check
169 * @param hidden the level where the check is performed
170 * @return true, if at least one non-inverted filter applies to the primitive
171 */
172 private static <T extends IFilterablePrimitive> boolean isFilterExplicit(T primitive, boolean hidden) {
173 return hidden ? primitive.getHiddenType() : primitive.getDisabledType();
174 }
175
176 /**
177 * Check if all parent ways are filtered.
178 * @param <T> The primitive type
179 * @param primitive the primitive to check
180 * @param hidden parameter that indicates the minimum level of filtering:
181 * true when objects need to be hidden to count as filtered and
182 * false when it suffices to be disabled to count as filtered
183 * @return true if (a) there is at least one parent way
184 * (b) all parent ways are filtered at least at the level indicated by the
185 * parameter <code>hidden</code> and
186 * (c) at least one of the parent ways is explicitly filtered
187 */
188 private static <T extends IPrimitive & IFilterablePrimitive> boolean allParentWaysFiltered(T primitive, boolean hidden) {
189 List<? extends IPrimitive> refs = primitive.getReferrers();
190 boolean isExplicit = false;
191 for (IPrimitive p: refs) {
192 if (p instanceof IWay && p instanceof IFilterablePrimitive) {
193 if (!isFiltered(p, hidden))
194 return false;
195 isExplicit |= isFilterExplicit((IFilterablePrimitive) p, hidden);
196 }
197 }
198 return isExplicit;
199 }
200
201 private static boolean oneParentWayNotFiltered(IPrimitive primitive, boolean hidden) {
202 return primitive.getReferrers().stream().filter(IWay.class::isInstance).map(IWay.class::cast)
203 .anyMatch(p -> !isFiltered(p, hidden));
204 }
205
206 private static boolean allParentMultipolygonsFiltered(IPrimitive primitive, boolean hidden) {
207 boolean isExplicit = false;
208 for (IRelation<?> r : new SubclassFilteredCollection<IPrimitive, IRelation<?>>(
209 primitive.getReferrers(), i -> i.isMultipolygon() && i instanceof IFilterablePrimitive)) {
210 if (!isFiltered(r, hidden))
211 return false;
212 isExplicit |= isFilterExplicit((IFilterablePrimitive) r, hidden);
213 }
214 return isExplicit;
215 }
216
217 private static boolean oneParentMultipolygonNotFiltered(IPrimitive primitive, boolean hidden) {
218 return new SubclassFilteredCollection<IPrimitive, IRelation>(primitive.getReferrers(), IPrimitive::isMultipolygon).stream()
219 .anyMatch(r -> !isFiltered(r, hidden));
220 }
221
222 private static <T extends IPrimitive & IFilterablePrimitive> FilterType test(List<FilterInfo> filters, T primitive, boolean hidden) {
223 if (primitive.isIncomplete() || primitive.isPreserved())
224 return FilterType.NOT_FILTERED;
225
226 boolean filtered = false;
227 // If the primitive is "explicitly" hidden by a non-inverted filter.
228 // Only interesting for nodes.
229 boolean explicitlyFiltered = false;
230
231 for (FilterInfo fi: filters) {
232 if (fi.isDelete) {
233 if (filtered && fi.match.match(primitive)) {
234 filtered = false;
235 }
236 } else {
237 if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) {
238 filtered = true;
239 if (!fi.isInverted) {
240 explicitlyFiltered = true;
241 }
242 }
243 }
244 }
245
246 if (primitive instanceof INode) {
247 if (filtered) {
248 // If there is a parent way, that is not hidden, we show the
249 // node anyway, unless there is no non-inverted filter that
250 // applies to the node directly.
251 if (explicitlyFiltered)
252 return FilterType.PASSIV;
253 else {
254 if (oneParentWayNotFiltered(primitive, hidden))
255 return FilterType.NOT_FILTERED;
256 else
257 return FilterType.PASSIV;
258 }
259 } else {
260 if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden))
261 // Technically not hidden by any filter, but we hide it anyway, if
262 // it is untagged and all parent ways are hidden.
263 return FilterType.PASSIV;
264 else
265 return FilterType.NOT_FILTERED;
266 }
267 } else if (primitive instanceof IWay) {
268 if (filtered) {
269 if (explicitlyFiltered)
270 return FilterType.EXPLICIT;
271 else {
272 if (oneParentMultipolygonNotFiltered(primitive, hidden))
273 return FilterType.NOT_FILTERED;
274 else
275 return FilterType.PASSIV;
276 }
277 } else {
278 if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden))
279 return FilterType.EXPLICIT;
280 else
281 return FilterType.NOT_FILTERED;
282 }
283 } else {
284 if (filtered)
285 return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV;
286 else
287 return FilterType.NOT_FILTERED;
288 }
289
290 }
291
292 /**
293 * Check if primitive is hidden.
294 * The filter flags for all parent objects must be set correctly, when
295 * calling this method.
296 * @param <T> The primitive type
297 * @param primitive the primitive
298 * @return FilterType.NOT_FILTERED when primitive is not hidden;
299 * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted
300 * filter that applies;
301 * FilterType.PASSIV when primitive is hidden and all filters that apply
302 * are inverted
303 */
304 public <T extends IPrimitive & IFilterablePrimitive> FilterType isHidden(T primitive) {
305 return test(hiddenFilters, primitive, true);
306 }
307
308 /**
309 * Check if primitive is disabled.
310 * The filter flags for all parent objects must be set correctly, when
311 * calling this method.
312 * @param <T> The primitive type
313 * @param primitive the primitive
314 * @return FilterType.NOT_FILTERED when primitive is not disabled;
315 * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted
316 * filter that applies;
317 * FilterType.PASSIV when primitive is disabled and all filters that apply
318 * are inverted
319 */
320 public <T extends IPrimitive & IFilterablePrimitive> FilterType isDisabled(T primitive) {
321 return test(disabledFilters, primitive, false);
322 }
323
324 /**
325 * Returns a new {@code FilterMatcher} containing the given filters.
326 * @param filters filters to add to the resulting filter matcher
327 * @return a new {@code FilterMatcher} containing the given filters
328 * @throws SearchParseError if the search expression in a filter cannot be parsed
329 * @since 12383
330 */
331 public static FilterMatcher of(Filter... filters) throws SearchParseError {
332 FilterMatcher result = new FilterMatcher();
333 for (Filter filter : filters) {
334 result.add(filter);
335 }
336 return result;
337 }
338}
Note: See TracBrowser for help on using the repository browser.