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