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 * Adds a filter to the currently used filters 117 * @param filter the filter to add 118 * @throws SearchParseError if the search expression in the filter cannot be parsed 119 */ 120 public void add(final Filter filter) throws SearchParseError { 121 if (!filter.enable) { 122 return; 123 } 124 125 FilterInfo fi = new FilterInfo(filter); 126 if (fi.isDelete) { 127 if (filter.hiding) { 128 // Remove only hide flag 129 hiddenFilters.add(fi); 130 } else { 131 // Remove both flags 132 disabledFilters.add(fi); 133 hiddenFilters.add(fi); 134 } 135 } else { 136 if (filter.mode == SearchMode.replace && filter.hiding) { 137 hiddenFilters.clear(); 138 disabledFilters.clear(); 139 } 140 141 disabledFilters.add(fi); 142 if (filter.hiding) { 143 hiddenFilters.add(fi); 144 } 145 } 146 } 147 148 /** 149 * Check if primitive is filtered. 150 * @param primitive the primitive to check 151 * @param hidden the minimum level required for the primitive to count as filtered 152 * @return when hidden is true, returns whether the primitive is hidden 153 * when hidden is false, returns whether the primitive is disabled or hidden 154 */ 155 private static boolean isFiltered(OsmPrimitive primitive, boolean hidden) { 156 return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled(); 157 } 158 159 /** 160 * Check if primitive is hidden explicitly. 161 * Only used for ways and relations. 162 * @param primitive the primitive to check 163 * @param hidden the level where the check is performed 164 * @return true, if at least one non-inverted filter applies to the primitive 165 */ 166 private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) { 167 return hidden ? primitive.getHiddenType() : primitive.getDisabledType(); 168 } 169 170 /** 171 * Check if all parent ways are filtered. 172 * @param primitive the primitive to check 173 * @param hidden parameter that indicates the minimum level of filtering: 174 * true when objects need to be hidden to count as filtered and 175 * false when it suffices to be disabled to count as filtered 176 * @return true if (a) there is at least one parent way 177 * (b) all parent ways are filtered at least at the level indicated by the 178 * parameter <code>hidden</code> and 179 * (c) at least one of the parent ways is explicitly filtered 180 */ 181 private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) { 182 List<OsmPrimitive> refs = primitive.getReferrers(); 183 boolean isExplicit = false; 184 for (OsmPrimitive p: refs) { 185 if (p instanceof Way) { 186 if (!isFiltered(p, hidden)) 187 return false; 188 isExplicit |= isFilterExplicit(p, hidden); 189 } 190 } 191 return isExplicit; 192 } 193 194 private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) { 195 List<OsmPrimitive> refs = primitive.getReferrers(); 196 for (OsmPrimitive p: refs) { 197 if (p instanceof Way && !isFiltered(p, hidden)) 198 return true; 199 } 200 201 return false; 202 } 203 204 private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) { 205 boolean isExplicit = false; 206 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 207 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 208 if (!isFiltered(r, hidden)) 209 return false; 210 isExplicit |= isFilterExplicit(r, hidden); 211 } 212 return isExplicit; 213 } 214 215 private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) { 216 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 217 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 218 if (!isFiltered(r, hidden)) 219 return true; 220 } 221 return false; 222 } 223 224 private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) { 225 if (primitive.isIncomplete() || primitive.isPreserved()) 226 return FilterType.NOT_FILTERED; 227 228 boolean filtered = false; 229 // If the primitive is "explicitly" hidden by a non-inverted filter. 230 // Only interesting for nodes. 231 boolean explicitlyFiltered = false; 232 233 for (FilterInfo fi: filters) { 234 if (fi.isDelete) { 235 if (filtered && fi.match.match(primitive)) { 236 filtered = false; 237 } 238 } else { 239 if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) { 240 filtered = true; 241 if (!fi.isInverted) { 242 explicitlyFiltered = true; 243 } 244 } 245 } 246 } 247 248 if (primitive instanceof Node) { 249 if (filtered) { 250 // If there is a parent way, that is not hidden, we show the 251 // node anyway, unless there is no non-inverted filter that 252 // applies to the node directly. 253 if (explicitlyFiltered) 254 return FilterType.PASSIV; 255 else { 256 if (oneParentWayNotFiltered(primitive, hidden)) 257 return FilterType.NOT_FILTERED; 258 else 259 return FilterType.PASSIV; 260 } 261 } else { 262 if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden)) 263 // Technically not hidden by any filter, but we hide it anyway, if 264 // it is untagged and all parent ways are hidden. 265 return FilterType.PASSIV; 266 else 267 return FilterType.NOT_FILTERED; 268 } 269 } else if (primitive instanceof Way) { 270 if (filtered) { 271 if (explicitlyFiltered) 272 return FilterType.EXPLICIT; 273 else { 274 if (oneParentMultipolygonNotFiltered(primitive, hidden)) 275 return FilterType.NOT_FILTERED; 276 else 277 return FilterType.PASSIV; 278 } 279 } else { 280 if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden)) 281 return FilterType.EXPLICIT; 282 else 283 return FilterType.NOT_FILTERED; 284 } 285 } else { 286 if (filtered) 287 return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV; 288 else 289 return FilterType.NOT_FILTERED; 290 } 291 292 } 293 294 /** 295 * Check if primitive is hidden. 296 * The filter flags for all parent objects must be set correctly, when 297 * calling this method. 298 * @param primitive the primitive 299 * @return FilterType.NOT_FILTERED when primitive is not hidden; 300 * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted 301 * filter that applies; 302 * FilterType.PASSIV when primitive is hidden and all filters that apply 303 * are inverted 304 */ 305 public FilterType isHidden(OsmPrimitive primitive) { 306 return test(hiddenFilters, primitive, true); 307 } 308 309 /** 310 * Check if primitive is disabled. 311 * The filter flags for all parent objects must be set correctly, when 312 * calling this method. 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 FilterType isDisabled(OsmPrimitive 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}