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

Last change on this file since 12494 was 12383, checked in by Don-vip, 7 years ago

see #14929 - new methods to ease the direct handling of filters

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