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.actions.search.SearchAction.SearchMode; 009import org.openstreetmap.josm.actions.search.SearchCompiler; 010import org.openstreetmap.josm.actions.search.SearchCompiler.Match; 011import org.openstreetmap.josm.actions.search.SearchCompiler.Not; 012import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; 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 ParseError { 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 ParseError if the search expression in one of the filters cannot be parsed 099 */ 100 public void update(Collection<Filter> filters) throws ParseError { 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 ParseError if the search expression in the filter cannot be parsed 119 */ 120 public void add(final Filter filter) throws ParseError { 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) { 137 if (filter.hiding) { 138 hiddenFilters.clear(); 139 disabledFilters.clear(); 140 } 141 } 142 143 disabledFilters.add(fi); 144 if (filter.hiding) { 145 hiddenFilters.add(fi); 146 } 147 } 148 } 149 150 /** 151 * Check if primitive is filtered. 152 * @param primitive the primitive to check 153 * @param hidden the minimum level required for the primitive to count as filtered 154 * @return when hidden is true, returns whether the primitive is hidden 155 * when hidden is false, returns whether the primitive is disabled or hidden 156 */ 157 private static boolean isFiltered(OsmPrimitive primitive, boolean hidden) { 158 return hidden ? primitive.isDisabledAndHidden() : primitive.isDisabled(); 159 } 160 161 /** 162 * Check if primitive is hidden explicitly. 163 * Only used for ways and relations. 164 * @param primitive the primitive to check 165 * @param hidden the level where the check is performed 166 * @return true, if at least one non-inverted filter applies to the primitive 167 */ 168 private static boolean isFilterExplicit(OsmPrimitive primitive, boolean hidden) { 169 return hidden ? primitive.getHiddenType() : primitive.getDisabledType(); 170 } 171 172 /** 173 * Check if all parent ways are filtered. 174 * @param primitive the primitive to check 175 * @param hidden parameter that indicates the minimum level of filtering: 176 * true when objects need to be hidden to count as filtered and 177 * false when it suffices to be disabled to count as filtered 178 * @return true if (a) there is at least one parent way 179 * (b) all parent ways are filtered at least at the level indicated by the 180 * parameter <code>hidden</code> and 181 * (c) at least one of the parent ways is explicitly filtered 182 */ 183 private static boolean allParentWaysFiltered(OsmPrimitive primitive, boolean hidden) { 184 List<OsmPrimitive> refs = primitive.getReferrers(); 185 boolean isExplicit = false; 186 for (OsmPrimitive p: refs) { 187 if (p instanceof Way) { 188 if (!isFiltered(p, hidden)) 189 return false; 190 isExplicit |= isFilterExplicit(p, hidden); 191 } 192 } 193 return isExplicit; 194 } 195 196 private static boolean oneParentWayNotFiltered(OsmPrimitive primitive, boolean hidden) { 197 List<OsmPrimitive> refs = primitive.getReferrers(); 198 for (OsmPrimitive p: refs) { 199 if (p instanceof Way && !isFiltered(p, hidden)) 200 return true; 201 } 202 203 return false; 204 } 205 206 private static boolean allParentMultipolygonsFiltered(OsmPrimitive primitive, boolean hidden) { 207 boolean isExplicit = false; 208 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 209 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 210 if (!isFiltered(r, hidden)) 211 return false; 212 isExplicit |= isFilterExplicit(r, hidden); 213 } 214 return isExplicit; 215 } 216 217 private static boolean oneParentMultipolygonNotFiltered(OsmPrimitive primitive, boolean hidden) { 218 for (Relation r : new SubclassFilteredCollection<OsmPrimitive, Relation>( 219 primitive.getReferrers(), OsmPrimitive::isMultipolygon)) { 220 if (!isFiltered(r, hidden)) 221 return true; 222 } 223 return false; 224 } 225 226 private static FilterType test(List<FilterInfo> filters, OsmPrimitive primitive, boolean hidden) { 227 228 if (primitive.isIncomplete()) 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}