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 && 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 226 if (primitive.isIncomplete()) 227 return FilterType.NOT_FILTERED; 228 229 boolean filtered = false; 230 // If the primitive is "explicitly" hidden by a non-inverted filter. 231 // Only interesting for nodes. 232 boolean explicitlyFiltered = false; 233 234 for (FilterInfo fi: filters) { 235 if (fi.isDelete) { 236 if (filtered && fi.match.match(primitive)) { 237 filtered = false; 238 } 239 } else { 240 if ((!filtered || (!explicitlyFiltered && !fi.isInverted)) && fi.match.match(primitive)) { 241 filtered = true; 242 if (!fi.isInverted) { 243 explicitlyFiltered = true; 244 } 245 } 246 } 247 } 248 249 if (primitive instanceof Node) { 250 if (filtered) { 251 // If there is a parent way, that is not hidden, we show the 252 // node anyway, unless there is no non-inverted filter that 253 // applies to the node directly. 254 if (explicitlyFiltered) 255 return FilterType.PASSIV; 256 else { 257 if (oneParentWayNotFiltered(primitive, hidden)) 258 return FilterType.NOT_FILTERED; 259 else 260 return FilterType.PASSIV; 261 } 262 } else { 263 if (!primitive.isTagged() && allParentWaysFiltered(primitive, hidden)) 264 // Technically not hidden by any filter, but we hide it anyway, if 265 // it is untagged and all parent ways are hidden. 266 return FilterType.PASSIV; 267 else 268 return FilterType.NOT_FILTERED; 269 } 270 } else if (primitive instanceof Way) { 271 if (filtered) { 272 if (explicitlyFiltered) 273 return FilterType.EXPLICIT; 274 else { 275 if (oneParentMultipolygonNotFiltered(primitive, hidden)) 276 return FilterType.NOT_FILTERED; 277 else 278 return FilterType.PASSIV; 279 } 280 } else { 281 if (!primitive.isTagged() && allParentMultipolygonsFiltered(primitive, hidden)) 282 return FilterType.EXPLICIT; 283 else 284 return FilterType.NOT_FILTERED; 285 } 286 } else { 287 if (filtered) 288 return explicitlyFiltered ? FilterType.EXPLICIT : FilterType.PASSIV; 289 else 290 return FilterType.NOT_FILTERED; 291 } 292 293 } 294 295 /** 296 * Check if primitive is hidden. 297 * The filter flags for all parent objects must be set correctly, when 298 * calling this method. 299 * @param primitive the primitive 300 * @return FilterType.NOT_FILTERED when primitive is not hidden; 301 * FilterType.EXPLICIT when primitive is hidden and there is a non-inverted 302 * filter that applies; 303 * FilterType.PASSIV when primitive is hidden and all filters that apply 304 * are inverted 305 */ 306 public FilterType isHidden(OsmPrimitive primitive) { 307 return test(hiddenFilters, primitive, true); 308 } 309 310 /** 311 * Check if primitive is disabled. 312 * The filter flags for all parent objects must be set correctly, when 313 * calling this method. 314 * @param primitive the primitive 315 * @return FilterType.NOT_FILTERED when primitive is not disabled; 316 * FilterType.EXPLICIT when primitive is disabled and there is a non-inverted 317 * filter that applies; 318 * FilterType.PASSIV when primitive is disabled and all filters that apply 319 * are inverted 320 */ 321 public FilterType isDisabled(OsmPrimitive primitive) { 322 return test(disabledFilters, primitive, false); 323 } 324 325}