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(OsmPrimitive 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 primitive the primitive to check 172 * @param hidden the level where the check is performed 173 * @return true, if at least one non-inverted filter applies to the primitive 174 */ 175 private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) { 176 return hidden ? primitive.getHiddenType() : primitive.getDisabledType(); 177 } 178 179 /** 180 * Check if all parent ways are filtered. 181 * @param primitive the primitive to check 182 * @param hidden parameter that indicates the minimum level of filtering: 183 * true when objects need to be hidden to count as filtered and 184 * false when it suffices to be disabled to count as filtered 185 * @return true if (a) there is at least one parent way 186 * (b) all parent ways are filtered at least at the level indicated by the 187 * parameter <code>hidden</code> and 188 * (c) at least one of the parent ways is explicitly filtered 189 */ 190 private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) { 191 List<OsmPrimitive> refs = primitive.getReferrers(); 192 boolean isExplicit = false; 193 for (OsmPrimitive p: refs) { 194 if (p instanceof Way) { 195 if (!isFiltered(p, hidden)) 196 return false; 197 isExplicit |= isFilterExplicit(p, hidden); 198 } 199 } 200 return isExplicit; 201 } 202 203 private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) { 204 List<OsmPrimitive> refs = primitive.getReferrers(); 205 for (OsmPrimitive p: refs) { 206 if (p instanceof Way && !isFiltered(p, hidden)) 207 return true; 208 } 209 210 return false; 211 } 212 213 private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) { 214 boolean isExplicit = false; 215 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 216 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 217 if (!isFiltered(r, hidden)) 218 return false; 219 isExplicit |= isFilterExplicit(r, hidden); 220 } 221 return isExplicit; 222 } 223 224 private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) { 225 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 226 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 227 if (!isFiltered(r, hidden)) 228 return true; 229 } 230 return false; 231 } 232 233 private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) { 234 if (primitive.isIncomplete() || primitive.isPreserved()) 235 return FilterType.NOT_FILTERED; 236 237 boolean filtered = false; 238 // If the primitive is "explicitly" hidden by a non-inverted filter. 239 // Only interesting for nodes. 240 boolean explicitlyFiltered = false; 241 242 for (FilterInfo fi: filters) { 243 if (fi.isDelete) { 244 if (filtered && fi.match.match(primitive)) { 245 filtered = false; 246 } 247 } else { 248 if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) { 249 filtered = true; 250 if (!fi.isInverted) { 251 explicitlyFiltered = true; 252 } 253 } 254 } 255 } 256 257 if (primitive instanceof Node) { 258 if (filtered) { 259 // If there is a parent way, that is not hidden, we show the 260 // node anyway, unless there is no non-inverted filter that 261 // applies to the node directly. 262 if (explicitlyFiltered) 263 return FilterType.PASSIV; 264 else { 265 if (oneParentWayNotFiltered(primitive, hidden)) 266 return FilterType.NOT_FILTERED; 267 else 268 return FilterType.PASSIV; 269 } 270 } else { 271 if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden)) 272 // Technically not hidden by any filter, but we hide it anyway, if 273 // it is untagged and all parent ways are hidden. 274 return FilterType.PASSIV; 275 else 276 return FilterType.NOT_FILTERED; 277 } 278 } else if (primitive instanceof Way) { 279 if (filtered) { 280 if (explicitlyFiltered) 281 return FilterType.EXPLICIT; 282 else { 283 if (oneParentMultipolygonNotFiltered(primitive, hidden)) 284 return FilterType.NOT_FILTERED; 285 else 286 return FilterType.PASSIV; 287 } 288 } else { 289 if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden)) 290 return FilterType.EXPLICIT; 291 else 292 return FilterType.NOT_FILTERED; 293 } 294 } else { 295 if (filtered) 296 return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV; 297 else 298 return FilterType.NOT_FILTERED; 299 } 300 301 } 302 303 /** 304 * Check if primitive is hidden. 305 * The filter flags for all parent objects must be set correctly, when 306 * calling this method. 307 * @param primitive the primitive 308 * @return FilterType.NOT_FILTERED when primitive is not hidden; 309 * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted 310 * filter that applies; 311 * FilterType.PASSIV when primitive is hidden and all filters that apply 312 * are inverted 313 */ 314 public FilterType isHidden(OsmPrimitive primitive) { 315 return test(hiddenFilters, primitive, true); 316 } 317 318 /** 319 * Check if primitive is disabled. 320 * The filter flags for all parent objects must be set correctly, when 321 * calling this method. 322 * @param primitive the primitive 323 * @return FilterType.NOT_FILTERED when primitive is not disabled; 324 * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted 325 * filter that applies; 326 * FilterType.PASSIV when primitive is disabled and all filters that apply 327 * are inverted 328 */ 329 public FilterType isDisabled(OsmPrimitive primitive) { 330 return test(disabledFilters, primitive, false); 331 } 332 333 /** 334 * Returns a new {@code FilterMatcher} containing the given filters. 335 * @param filters filters to add to the resulting filter matcher 336 * @return a new {@code FilterMatcher} containing the given filters 337 * @throws SearchParseError if the search expression in a filter cannot be parsed 338 * @since 12383 339 */ 340 public static FilterMatcher of(Filter... filters) throws SearchParseError { 341 FilterMatcher result = new FilterMatcher(); 342 for (Filter filter : filters) { 343 result.add(filter); 344 } 345 return result; 346 } 347}