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